面試官:小明,是吧?你都知道哪些排序算法,哪幾種是穩定排序?
小明:這個我有總結!
關於排序穩定性的定義
通俗地講就是能保證排序前兩個相等的數其在序列的前后位置順序和排序后它們兩個的前后位置順序相同。在簡單形式化一下,如果Ai = Aj,Ai原來在位置前,排序后Ai還是要在Aj位置前。
現實生活中穩定排序如何表現呢?
舉個例子:
某次學校發獎學金,只有排在前三個的有獎,結果一排序把原來在第三位的並列第三名給弄到第四位了,他估計不會樂意😂
接下來給大家用java代碼演繹一下常見的幾種排序,前提:有一個數組arr,要求從小到大排序。
選擇排序
簡單選擇排序的思想是:從第一個位置開始,逐漸向后,選擇后面的無序序列中的最小值放到該位置。很簡單,直接上代碼吧:
//選擇排序
for(int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
int k = i;
for(int j = k + 1; j < arr.length; j++){// 選最小的記錄
if(arr[j] < arr[k]){
k = j; //記下目前找到的最小值所在的位置
}
}
//在內層循環結束,也就是找到本輪循環的最小的數以后,再進行交換
if(i != k){ //交換a[i]和a[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
那選擇排序是穩定排序嗎?
舉個例子,假如有序列[5,8,5,2,9]按從小到大排序,第一遍排序,第一個元素“5”會和第四個元素“2”交換,那么原序列中兩個“5”的相對前后順序此時就遭到破壞了,由此可見,選擇排序不是一個穩定的排序算法。
冒泡排序
冒泡排序就是相鄰的兩個元素之間按照要求進行比較交換,代碼如下:
// 冒泡排序
for (int i = 0; i < arr.length - 1; i++) { //外層循環n-1
for (int j = 0; j < arr.length - i - 1; j++) { //內層循環n-i-1
if (arr[j] > arr[j + 1]) { //從第一個開始,往后兩兩比較大小,如果前面的比后面的大,交換位置
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
那冒泡排序是穩定排序嗎?
因為發生在相鄰的元素之間,所以,如果兩個元素相等,我們是不會多此一舉把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那么即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前后順序並沒有改變,所以冒泡排序是一種穩定排序算法。
插入排序
插入排序與打撲克時整理手上的牌非常類似。摸來的第1張牌無須整理,此后每次從桌上的牌(無序區)中摸最上面的1張並插入左手的牌(有序區)中正確的位置上。為了找到這個正確的位置,須自左向右(或自右向左)將摸來的牌與左手中已有的牌逐一比較,代碼如下:
// 插入排序
for (int index = 1; index < length; index++) { //外層向右的index,即作為比較對象的數據的index
int temp = arr[index]; //用作比較的數據
int leftindex = index - 1;
while (leftindex >= 0 && arr[leftindex] > temp) { //當比到最左邊或者遇到比temp小的數據時,結束循環
arr[leftindex + 1] = arr[leftindex];
leftindex--;
}
arr[leftindex + 1] = temp;//把temp放到空位上
}
一開始,左邊會產生一個只有一個元素的有序序列,比較是從該有序序列的末尾開始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其后面,否則一直往前找直到找到它該插入的位置。
那插入排序是穩定排序嗎?
如果碰見一個和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后順序沒有改變,從原無序序列出去的順序就是排好序后的順序,所以插入排序是穩定的。
快速排序
快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index]
,其中center_index
是中樞元素的數組下標,一般取為數組第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]
。如果i和j都走不動了,i <= j
,交換a[i]
和a[j]
,重復上面的過程,直到i > j
。 交換a[j]
和a[center_index]
,完成一趟快速排序。在中樞元素和a[j]
交換的時候,很有可能把前面的元素的穩定性打亂,代碼如下:
// 快速排序
public static void sort(int[] a, int low, int height) {
int i = low;
int j = height;
if (i > j) { //放在k之前,防止下標越界
return;
}
int k = a[i];
while (i < j) {
while (i < j && a[j] > k) { //找出小的數
j--;
}
while (i < j && a[i] <= k) { //找出大的數
i++;
}
if (i < j) { //交換
int swap = a[i];
a[i] = a[j];
a[j] = swap;
}
}
//交換K
k = a[i];
a[i] = a[low];
a[low] = k;
//對左邊進行排序,遞歸算法
sort(a, low, i - 1);
//對右邊進行排序
sort(a, i + 1, height);
}
那快速排序是穩定排序嗎?
比如序列為5 3 3 4 3 8 9 10 11,現在中樞元素5和3(第五個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序算法,不穩定發生在中樞元素和a[j] 交換的時刻。
歸並排序
歸並排序是把序列遞歸地分成短序列,遞歸出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然后把各個有序的段序列合並成一個有序的長序列,不斷合並直到原序列全部排好序,代碼如下:
// 歸並排序
public class Main {
public static void main(String[] args) {
int[] arr = {11,44,23,67,88,65,34,48,9,12};
int[] tmp = new int[arr.length]; //新建一個臨時數組存放
mergeSort(arr,0,arr.length-1,tmp);
for(int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
}
public static void merge(int[] arr,int low,int mid,int high,int[] tmp){
int i = 0;
int j = low,k = mid+1; //左邊序列和右邊序列起始索引
while(j <= mid && k <= high){
if(arr[j] < arr[k]){
tmp[i++] = arr[j++];
}else{
tmp[i++] = arr[k++];
}
}
//若左邊序列還有剩余,則將其全部拷貝進tmp[]中
while(j <= mid){
tmp[i++] = arr[j++];
}
while(k <= high){
tmp[i++] = arr[k++];
}
for(int t=0;t<i;t++){
arr[low+t] = tmp[t];
}
}
public static void mergeSort(int[] arr,int low,int high,int[] tmp){
if(low<high){
int mid = (low+high)/2;
mergeSort(arr,low,mid,tmp); //對左邊序列進行歸並排序
mergeSort(arr,mid+1,high,tmp); //對右邊序列進行歸並排序
merge(arr,low,mid,high,tmp); //合並兩個有序序列
}
}
}
那歸並排序是穩定排序嗎?
可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定性。那么,在短的有序序列合並的過程中,穩定是是否受到破壞?沒有,合並過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素保存在結果序列的前面,這樣就保證了穩定性。所以,歸並排序也是穩定的排序算法。
基數排序(又稱桶子法)
基數排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次類推,直到最高位。
代碼如下:
// 基數排序(又稱桶子法)
public static void myRadixSort(int[] arr) {
int max = 0;
// 找到最大的數
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
// 獲取最大數的位數
int times = 0;
while (max > 0) {
max = max / 10;
times++;
}
// 創建一個二維的list
List<ArrayList> list = new ArrayList<>();
// 創建10個list(每一位有從0到9,一共10個數,每個list數組用來存放每次迭代中,0-9 每個數組中需要裝入的數)
for (int i = 0; i < 10; i++) {
ArrayList list1 = new ArrayList();
//在二維數組中把這10個數組加進去,相當於二維數組的行,從0-9的行
list.add(list1);
}
// 進行times次分配和收集
for (int i = 0; i < times; i++) {
// 分配
for (int j = 0; j < arr.length; j++) {
int x = arr[j] % (int) Math.pow(10, i + 1) / (int) Math.pow(10, i);
// list.get(x) 是在返回第0的這個行的list上面的數,然后再 add(arr[j]) 是把當前的這個數添加到末尾去
list.get(x).add(arr[j]);
}
// 收集 ------------> 把這0-9共10個list里面的數值存到一個數組里面
int count = 0;
for (int j = 0; j < 10; j++) {
while (list.get(j).size() > 0) {
// 把list這個二維list中的第j行返回並賦值給list2
ArrayList<Integer> list2 = list.get(j);
// 把list2這個數組中的第0個位置的元素,賦值給arr[count]
arr[count] = list2.get(0);
// 把list2這個數組中的第0個位置的元素刪除掉,則后面的元素會自動移上來
list2.remove(0);
count++;
}
}
}
}
那基數排序是穩定排序嗎?
由上可得,基數排序基於分別排序,分別收集,所以其是穩定的排序算法。
希爾排序
希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小, 插入排序對於有序的序列效率很高,所以,希爾排序的時間復雜度會比O(n^2)好一些。代碼如下:
// 希爾排序
int incrementNum = arr.length / 2;
while (incrementNum >= 1) {
for (int i = 0; i < arr.length; i++) {
//進行插入排序
for (int j = i; j < arr.length - incrementNum; j = j + incrementNum) {
if (arr[j] > arr[j + incrementNum]) {
int temple = arr[j];
arr[j] = arr[j + incrementNum];
arr[j + incrementNum] = temple;
}
}
}
//設置新的增量
incrementNum = incrementNum / 2;
}
那基數排序是穩定排序嗎?
由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最后其穩定性就會被打亂,所以shell排序是不穩定的。
堆排序
堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了,代碼舉例如下:
// 堆排序
public static void sort(int[] arr) {
//1.構建大頂堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
//從第一個非葉子結點從下至上,從右至左調整結構
adjustHeap(arr, i, arr.length);
}
//2.調整堆結構+交換堆頂元素與末尾元素
for (int j = arr.length - 1; j > 0; j--) {
swap(arr, 0, j);//將堆頂元素與末尾元素進行交換
adjustHeap(arr, 0, j);//重新對堆進行調整
}
}
/**
* 調整大頂堆(僅是調整過程,建立在大頂堆已構建的基礎上)
*
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int[] arr, int i, int length) {
int temp = arr[i];//先取出當前元素i
for (int k = i * 2 + 1; k < length; k = k * 2 + 1) {//從i結點的左子結點開始,也就是2i+1處開始
if (k + 1 < length && arr[k] < arr[k + 1]) {//如果左子結點小於右子結點,k指向右子結點
k++;
}
if (arr[k] > temp) {//如果子節點大於父節點,將子節點值賦給父節點(不用進行交換)
arr[i] = arr[k];
i = k;
} else {
break;
}
}
arr[i] = temp;//將temp值放到最終的位置
}
/**
* 交換元素
*
* @param arr
* @param a
* @param b
*/
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
那堆排序是穩定排序嗎?
我們知道堆的結構是節點i的孩子為2 * i
和2 * i + 1
節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長為n 的序列,堆排序的過程是從第n / 2
開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當為n / 2 - 1, n / 2 - 2, ... 1
這些個父節點選擇元素時,就會破壞穩定性。有可能第n / 2
個父節點交換把后面一個元素交換過去了,而第n / 2 - 1
個父節點把后面一個相同的元素沒 有交換,那么這2個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序算法。
總結
- 不穩定的排序: 選擇排序、快速排序、希爾排序、堆排序
- 穩定排序:冒泡排序、插入排序、歸並排序、基數排序