重學數據結構和算法(四)之冒泡排序、插入排序、選擇排序


最近學習了極客時間的《數據結構與算法之美》很有收獲,記錄總結一下。
歡迎學習老師的專欄:數據結構與算法之美
代碼地址:https://github.com/peiniwan/Arithmetic

排序

我們知道,時間復雜度反應的是數據規模 n 很大的時候的一個增長趨勢,所以它表示的時候會忽略系數、常數、低階。但是實際的軟件開發中,我們排序的可能是 10 個、100 個、1000 個這樣規模很小的數據,所以,在對同一階時間復雜度的排序算法性能對比的時候,我們就要把系數、常數、低階也考慮進來。

基於比較的排序算法的執行過程,會涉及兩種操作,一種是元素比較大小,另一種是元素交換或移動。所以,如果我們在分析排序算法的執行效率的時候,應該把比較次數和交換(或移動)次數也考慮進去。

排序算法的內存消耗
算法的內存消耗可以通過空間復雜度來衡量,排序算法也不例外。不過,針對排序算法的空間復雜度,我們還引入了一個新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空間復雜度是 O(1) 的排序算法。冒泡、插入、選擇,都是原地排序算法

排序算法的穩定性
針對排序算法,我們還有一個重要的度量指標,穩定性。這個概念是說,如果待排序的序列中存在值相等的元素,經過排序之后,相等元素之間原有的先后順序不變。
我通過一個例子來解釋一下。比如我們有一組數據 2,9,3,4,8,3,按照大小排序之后就是 2,3,3,4,8,9。這組數據里有兩個 3。
經過某種排序算法排序之后,如果兩個 3 的前后順序沒有改變,那我們就把這種排序算法叫作穩定的排序算法;如果前后順序發生變化,那對應的排序算法就叫作不穩定的排序算法。

穩定排序算法可以保持金額相同的兩個對象,在排序之后的前后順序不變

  • 穩定排序有:插入排序,基數排序,歸並排序 ,冒泡排序 ,基數排序。
  • 不穩定的排序算法有:快速排序,希爾排序,簡單選擇排序,堆排序。
  • 排序的穩定性,就是指,在對a關鍵字排序后會不會改變其他關鍵字的順序。

冒泡排序(Bubble Sort)

穩定、原地排序
我們要對一組數據 4,5,6,3,2,1,從小到大進行排序。經過一次冒泡操作之后,6 這個元素已經存儲在正確的位置上。要想完成所有數據的排序,我們只要進行 6 次這樣的冒泡操作就行了。

實際上,剛講的冒泡過程還可以優化。當某次冒泡操作已經沒有數據交換時,說明已經達到完全有序,不用再繼續執行后續的冒泡操作。我這里還有另外一個例子,這里面給 6 個元素排序,只需要 4 次冒泡操作就可以了。

// 冒泡排序,a表示數組,n表示數組大小
public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 提前退出冒泡循環的標志位
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) { //-x:比較元素減少,-1:避免角標越界  
      if (a[j] > a[j+1]) { // 交換
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;  // 表示有數據交換      
      }
    }
    if (!flag) break;  // 沒有數據交換,提前退出
  }
}

插入排序(Insertion Sort)

穩定、原地排序
基本思想是每一步將一個待排序的記錄,插入到前面已經排好序的有序序列中去,直到插完所有元素為止。

一個有序的數組,我們往里面添加一個新的數據后,如何繼續保持數據有序呢?很簡單,我們只要遍歷數組,找到數據應該插入的位置將其插入即可。

這是一個動態排序的過程,即動態地往有序集合中添加數據,我們可以通過這種方法保持集合中的數據一直有序。而對於一組靜態數據,我們也可以借鑒上面講的插入方法,來進行排序,於是就有了插入排序算法。

插入排序具體是如何借助上面的思想來實現排序的呢?

首先,我們將數組中的數據分為兩個區間,已排序區間和未排序區間。初始已排序區間只有一個元素,就是數組的第一個元素。插入算法的核心思想是取未排序區間中的元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間數據一直有序。重復這個過程,直到未排序區間中元素為空,算法結束。

要排序的數據是 4,5,6,1,3,2,其中左側為已排序區間,右側是未排序區間。

插入排序也包含兩種操作,一種是元素的比較,一種是元素的移動。
當我們需要將一個數據 a 插入到已排序區間時,需要拿 a 與已排序區間的元素依次比較大小,找到合適的插入位置。找到插入點之后,我們還需要將插入點之后的元素順序往后移動一位,這樣才能騰出位置給元素 a 插入。

// 插入排序,a表示數組,n表示數組大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;

  for (int i = 1; i < n; ++i) {
      //待插入元素
    int value = a[i];
    int j = i - 1;
    // 查找插入的位置
    for (; j >= 0; --j) {
      if (a[j] > value) {
        a[j+1] = a[j];  // 數據移動,將大於temp的往后移動一位
      } else {
        break;
      }
    }
    a[j+1] = value; // 插入數據
  }
}

插入排序和冒泡排序的時間復雜度相同,都是 O(n2),在實際的軟件開發里,為什么我們更傾向於使用插入排序算法而不是冒泡排序算法呢?
冒泡排序的數據交換要比插入排序的數據移動要復雜,冒泡排序需要 3 個賦值操作,而插入排序只需要 1 個。我們來看這段操作:冒泡排序中數據的交換操作:

冒泡排序中數據的交換操作:
if (a[j] > a[j+1]) { // 交換
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}

插入排序中數據的移動操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 數據移動
} else {
  break;
}

我們把執行一個賦值語句的時間粗略地計為單位時間(unit_time),然后分別用冒泡排序和插入排序對同一個逆序度是 K 的數組進行排序。用冒泡排序,需要 K 次交換操作,每次需要 3 個賦值語句,所以交換操作總耗時就是 3* K 單位時間。而插入排序中數據移動操作只需要 K 個單位時間。

二分法插入排序

二分法插入排序是在插入第i個元素時,對前面的0~i-1元素進行折半,先跟他們中間的那個元素比,如果小,則對前半再進行折半,否則對后半進行折半,直到left>right,然后以左下標為標准,左及左后邊全部后移,然后左位置前插入該數據。

二分法沒有排序,只有查找。所以當找到要插入的位置時。移動必須從最后一個記錄開始,向后移動一位,再移動倒數第2位,直到要插入的位置的記錄移后一位。

    private static void sort(int[] a) {
        // {4, 6, 8, 7, 3, 5, 9, 1}
        // {4, 6, 7, 8, 3, 5, 9, 1}
        for (int i = 1; i < a.length; i++) {
            int temp = a[i];//7
            int left = 0;
            int right = i - 1;//2
            int mid = 0;
            //確定(找到)要插入的位置
            while (left <= right) {
                //先獲取中間位置
                mid = (left + right) / 2;
                if (temp < a[mid]) {
                    //如果值比中間值小,讓right左移到中間下標-1,舍棄右邊
                    right = mid - 1;
                } else {//7  6
                    //如果值比中間值大,讓left右移到中間下標+1,舍棄左邊
                    left = mid + 1;//2
                }
            }
            for (int j = i - 1; j >= left; j--) {
                //以左下標為標准,左及左后邊全部后移,然后左位置前插入該數據。
                a[j + 1] = a[j];
            }
            if (left != i) {//如果相等,不需要移動
                //左位置插入該數據
                a[left] = temp;
            }
        }
    }

希爾排序(O(n^1.3))

  • 希爾排序也是一種插入排序,它是簡單插入排序經過改進之后的一個更高效的版本,也稱為縮小增量排序,同時該算法是沖破O(n2)的第一批算法之一。
  • 希爾排序是把記錄按下標的一定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減少,每組包含的關鍵詞越來越多,當增量減至1時,整個文件恰被分成一組,算法便終止。
  • 先取一個小於n的整數d1作為第一個增量,把數組的全部記錄分組。所有距離為d1的倍數的記錄放在同一個組中。先在各組內進行直接插入排序;然后,取第二個增量d2<d1重復上述的分組和排序,直至所取的增量 =1( < …<d2<d1),即所有記錄放在同一組中進行直接插入排序為止。
    public void heer(int[] a) {
        int d = a.length / 2;//默認增量
        while (true) {
            for (int i = 0; i < d; i++) {
                for (int j = i; j + d < a.length; j += d) {
                    //i=0  j=0,4
                    //i=1  j=1,5
                    int temp;
                    if (a[j] > a[j + d]) {
                        temp = a[j];
                        a[j] = a[j + d];
                        a[j + d] = temp;
                    }
                }
            }
            if (d == 1) {
                break;
            }
            d--;
        }
    }

選擇排序(Selection Sort)

基本思想為每一趟從待排序的數據元素中選擇最小(或最大)的一個元素作為首元素,直到所有元素排完為止
選擇排序算法的實現思路有點類似插入排序,也分已排序區間和未排序區間。但是選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾。

那選擇排序是穩定的排序算法嗎?
比如 5,8,5,2,9 這樣一組數據,使用選擇排序算法來排序的話,第一次找到最小元素 2,與第一個 5 交換位置,那第一個 5 和中間的 5 順序就變了,所以就不穩定了。正是因此,相對於冒泡排序和插入排序,選擇排序就稍微遜色了。

    public void selectSort(int[] array) {
        int min;
        int tmp;
        for (int i = 0; i < array.length; i++) {
            min = array[i];
            //里面for第一次出來0,並且排在最前面,然后從i=1開始遍歷
            for (int j = i; j < array.length; j++) {
                if (array[j] < min) {
                    min = array[j];//記錄最小值  3
                    tmp = array[i];//9
                    array[i] = min;//3
                    array[j] = tmp;//9
                }
            }
        }
        for (int num : array) {
            System.out.println(num);
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM