排序算法代碼總結

目錄


前言

排序算法在筆試面試中幾乎是必考的,因爲它是很多複雜算法的基礎,也是我們學習數據結構與算法的入門知識。目前網上介紹各類排序算法的博客和帖子非常多,但其中有不少作者提供的代碼有錯誤或者代碼中沒有考慮特殊情況。在此,我們再次總結一下常見的八大排序算法,試圖講清楚各個算法的基本原理,並提供java代碼以及詳細的註釋。所有代碼都是對數組進行升序排列且已經測試通過,但沒有使用足夠的測試用例,如果代碼中存在問題,請大家留言指出,或者有更好的算法思路,也可以交流討論。


冒泡排序

/**
 * 冒泡排序
 * 
 * 原理:
 * 每一輪循環中依次比較相鄰位置上元素的大小,使得較大的元素後移,
 * 且確保第i輪循環完之後能把第i大的元素移動到排序後的位置上
 * 
 * 改進:
 * 每一輪循環開始前設置標誌位,如果本輪循環沒有交換任何元素,
 * 則說明所有元素已經有序,可以提前結束排序
 * 
 * @author Roamer
 */
void bubbleSort(int[] a) {
    if(a == null || a.length == 0) return;

    boolean flag;
    for(int i = a.length - 1; i > 0; --i) {
        flag = true;//每一輪冒泡之前重置標誌位
        for(int j = 0; j < i; ++j) {
            if(a[j] > a[j+1]) {
                swap(a, j, j+1);
                flag = false;
            }
        }

        if(flag) break;
    }
}

選擇排序

/**
 * 選擇排序
 * 
 * 原理:
 * 從數組的首元素開始,將第i個元素之後的所有元素通過相互比較找到最小值的索引,
 * 如果當前元素比這個最小元素大,則交換之,使得第i個位置的元素值爲第i小,
 * 相當於每一輪循環是在所有未排序的元素之中選擇出最小的元素。
 * 
 * @author Roamer
 */
void selectSort(int[] a) {
    if(a == null || a.length == 0) return;

    for(int i = 0; i < a.length - 1; ++i) {
        //找到第i個元素之後的最小元素的下標minIdx
        int minIdx = i + 1;
        for(int j = i + 2; j < a.length; ++j) {
            if(a[j] < a[minIdx])
                minIdx = j;
        }
        //如果當前元素比其後的最小元素還小,則交換之
        if(a[i] > a[minIdx])
            swap(a, i, minIdx);
    }
}

插入排序

/**
 * 插入排序
 * 
 * 原理:
 * 確保數組前i-1個元素已經排好序,在第i輪循環時,將第i個元素從後往前依次和
 * 其前面的元素比較和交換,最後插入到前i-1個有序的子數組中的合適位置
 * 
 * @author Roamer
 */
void insertSort(int[] a) {
    if(a == null || a.length == 0) return;

    //從數組第二個元素開始進行前插
    for(int i = 1; i < a.length; ++i) {
        for(int j = i; j > 0 && a[j-1] > a[j]; --j)
                swap(a, j-1, j);
    }
}

/**
 * 插入排序改進
 * 
 * 如果當前元素已經位於正確的位置,則不必繼續往前插入,可以提前結束本輪循環
 * 
 * @author liwendongyang
 */
void insertSort2(int[] a) {
    if (a == null || a.length == 0)
        return;

    // 從數組第二個元素開始進行前插
    for (int i = 1; i < a.length; ++i) {
        for (int j = i; j > 0; --j) {
            if (a[j - 1] > a[j])
                swap(a, j - 1, j);
            else
                break;
        }
    }
}

快速排序

/**
 * 快速排序
 * 
 * 原理:
 * 將數組的首元素作爲比較的基準,用兩個指針從數組的兩端往中間掃描,
 * 當左指針對應的元素大於基準值且右指針對應的元素小於基準值,則交換兩者對應的元素值,
 * 使得每一輪遍歷之後數組分成比基準值更大和更小的兩部分,
 * 再把基準元素從數組首位交換到數組的中間,從而其左邊的元素都不大於它,
 * 且其右邊的元素都不小於它,然後將左右兩個子數組遞歸調用快排函數,最終使數組有序
 * 
 * @author Roamer
 */
void quickSort(int[] a, int low, int high) {
    if(a == null || a.length == 0) return;

    if(low < high){  
        int pivot = partition(a, low, high);
        quickSort(a, low, pivot - 1);  
        quickSort(a, pivot + 1, high);
    }
}

//版本一:兩個指針互相交換不合格元素(交換之後就都合格了),最後將基準元素移動到中間
private int partition(int[] a, int low, int high) {
    //將數組首元素作爲每一輪比較的基準
    int pivot = low;

    while(low < high) {
        //從右往左掃描,直到遇到比基準元素小的元素
        while(low < high && a[high] >= a[pivot])
            --high;

        //從左往右掃描,直到遇到比基準元素大的元素
        while(low < high && a[low] <= a[pivot])
            ++low;

        //將左子數組中不合格的元素與右子數組中不合格的元素交換
        swap(a, low, high); 
    }

    //將數組首元素交換到中間位置
    swap(a, pivot, low);

    //返回數組的中軸位置
    return low;
}


//版本一的改進版:
//由於基準元素已經保存了,所以其位置可以被覆蓋掉,且兩個指針是交替掃描的,
//所以右邊(左邊)的不合格元素可以直接覆蓋左邊(右邊)的不合格元素,
//由於high指針先掃描,然後兩個指針的元素交替覆蓋對方,所以循環結束後,
//low對應的位置還沒有被覆蓋,且它就是兩個子數組的分界,將基準元素放到此位置即可
private int partition1(int[] a, int low, int high) {
    //將數組首元素作爲每一輪比較的基準
    int pivotValue = a[low];

    while(low < high) {
        //從右往左掃描,直到遇到比基準元素小的元素
        while(low < high && a[high] >= pivotValue)
            --high;

        //將右子數組中不合格的元素放到左邊不合格元素的位置(原元素已經移走)
        a[low] = a[high];

        //從左往右掃描,直到遇到比基準元素大的元素
        while(low < high && a[low] <= pivotValue)
            ++low;

        //將左子數組中不合格的元素放到左邊不合格元素的位置(原元素已經移走) 
        a[high] = a[low];
    }

    //將基準元素放到中間位置
    a[low] = pivotValue;

    //返回數組的中軸位置
    return low;
}


//版本二:保持兩個指針中總有一個指向基準元素,所以每次交換都是不合格元素與基準元素做交換,
//當兩個指針在數組中間相遇時,low一定指向着基準元素
private int partition2(int[] a, int low, int high) {
    //將數組首元素作爲每一輪比較的基準
    int pivotValue = a[low];

    while(low < high) {
        //從右往左掃描,直到遇到比基準元素小的元素
        while(low < high && a[high] >= pivotValue)
            --high;

        //將右子數組中不合格的元素與基準元素交換
        swap(a, low, high);

        //從左往右掃描,直到遇到比基準元素大的元素
        while(low < high && a[low] <= pivotValue)
            ++low;

        //將左子數組中不合格的元素與基準元素交換
        swap(a, low, high); 
    }

    //返回數組的中軸位置,low必定指向了基準元素pivotValue
    return low;
}

歸併排序

/**
 * 歸併排序(遞歸版)
 * 
 * 原理:
 * 將數組均分成兩個子數組,先將兩個子數組分別進行排序,然後合併得到全體元素都有序的數組,
 * 爲使上述的兩個子數組分別有序,需要先對其各自的兩個子數組進行排序再合併,因此需要遞歸地
 * 對每個子數組的兩個子數組進行歸併排序,直到子數組只有2個元素,此時只需要直接進行合併
 * 
 * @author Roamer
 */
void MergeSort(int[] a) {
    if(a == null || a.length == 0) return;

    int[] b = new int[a.length];//輔助數組
    Merge(a, b, 0, a.length-1, (a.length-1)/2);
}

//對數組a的兩個子數組進行歸併排序
private void Merge(int[] a, int[] b, int low, int high, int pivot) {
    //先遞歸地劃分子數組(子數組最小長度爲2),並對子數組進行歸併排序
    if(low < high) {
        Merge(a, b, low, pivot, (low+pivot)/2);
        Merge(a, b, pivot+1, high, (high+pivot+1)/2);
    }

    //將已經排好序的兩個子數組元素依次進行比較再合併
    int i = low;
    int j = pivot+1;
    int k = low;
    while(i <= pivot && j <= high){
        if(a[i] < a[j])
            b[k++] = a[i++];
        else
            b[k++] = a[j++];
    }

    //取出子數組中可能的剩餘元素(每次只可能有一個while執行)
    while(i <= pivot)  b[k++] = a[i++];
    while(j <= high)  b[k++] = a[j++];  

    //將本次排好序的部分元素拷貝回原數組a
    System.arraycopy(b, low, a, low, high-low+1);
}


/**
 * 歸併排序(迭代版)
 * 
 * 原理:
 * 先將整個數組依次劃分成若干個長度爲2的子數組,對每個子數組中的兩個元素進行合併,
 * 再將整個數組依次劃分成若干個長度爲4的子數組,對每個子數組中的兩個子數組進行合併,
 * 如此循環,直到只能將數組劃分成兩個子數組,這兩個子數組已經分別有序,直接合並即可
 * 
 * @author Roamer
 */
//利用分治策略,對數組a的各級子數組進行迭代歸併
void MergeSort2(int[] a) {
    if(a == null || a.length == 0) return;

    int[] b = new int[a.length];//輔助數組
    int len = 2;//每一輪合併中數組的長度

    while(len <= a.length) {
        //將前若干組中兩個等長的子數組合並
        int i = 0;
        while(i + len <= a.length) {
            Merge2(a, b, i, i+len-1, i+(len-1)/2);
            i += len;
        }

        //若原數組長度不是2的冪,則數組可能不能被均分
        //從而最後一組的兩個子數組長度會不同,單獨合併之
        if(i != a.length)
            Merge2(a, b, i, a.length-1, (i+a.length)/2);

        //下一輪合併中數組的長度翻倍
        len <<= 1;

        //將本輪分組有序的元素拷貝回原數組a
        System.arraycopy(b, 0, a, 0, a.length);
    }

    //若原數組長度不是2的冪,需要最後合併一次!
    if(len != a.length) {
        Merge2(a, b, 0, a.length-1, (len-1)/2);
        System.arraycopy(b, 0, a, 0, a.length);
    }
}

private void Merge2(int[] a, int[] b, int low, int high, int pivot) {
    //將已經排好序的兩個子數組元素依次進行比較再合併
    int i = low;
    int j = pivot+1;
    int k = low;
    while(i <= pivot && j <= high){
        if(a[i] < a[j])
            b[k++] = a[i++];
        else
            b[k++] = a[j++];
    }

    //取出子數組中可能的剩餘元素(每次只可能有一個while執行)
    while(i <= pivot)  b[k++] = a[i++];
    while(j <= high)  b[k++] = a[j++];
}

堆排序

/**
 * 堆排序
 * 
 * 原理:
 * 先將無序數組構成一個二叉堆(完全二叉樹),使得每個節點都小於其子節點(兄弟節點之間可以無序),
 * 每次取出根節點後,重新調整堆使其仍滿足上述特性,每次取出的根節點就構成了一個有序數組。
 * 
 * 建堆
 * 用數組來存放整個二叉堆,且數組的首元素中存儲着整個二叉堆的節點總個數,
 * 建堆時總是在二叉堆的葉子節點處插入新節點,然後“上浮”該節點,最終在數組中得到一個二叉堆。
 * 
 * 調整堆
 * 每次在葉子節點處插入新節點即在數組末位插入新元素需要調整堆,比較新節點和其父節點的大小,
 * 通過不斷交換使其“上浮”,並最終位於二叉樹的合適位置;
 * 每次取出二叉堆的根節點即取出數組的第二個元素後需要調整堆,將二叉樹的最後一個葉子節點作爲新的根節點,
 * 然後依次比較其和子節點的大小,通過不斷交換使其“下沉”,並最終位於二叉樹的合適位置;
 * 
 * @author Roamer
 */
void heapSort(int[] a) {
    int[] b = new int[a.length + 1];//二叉堆數組

    //建堆
    for(int i = 0; i < a.length; ++i)
        insert(b, a[i]);

    //得到有序數組
    for(int i = 0; i < a.length; ++i)
        a[i] = getRoot(b);
}


//往二叉堆插入新節點,並調整堆
private void insert(int[] heap, int ele) {
    heap[0] += 1;//節點總數加1
    heap[heap[0]] = ele;

    goUp(heap);
}

//取出二叉堆的根節點,並調整堆
private int getRoot(int[] heap) {
    if (heap[0] < 1) 
        throw new RuntimeException("二叉堆已經爲空,不能再取出元素!");

    int root = heap[1];//取出根節點元素
    heap[1] = heap[heap[0]];//將二叉堆的最後一個葉子節點作爲新的根節點
    heap[0] -= 1;//節點總數減1

    goDown(heap);

    return root;
}

//根節點"下沉"
private void goDown(int[] heap) {
    int idx = 1;//需要下沉的根節點的索引
    int left, right, minIdx;//minIdx表示左右子節點中較小的那個
    boolean flag = true;//是否有元素交換的標誌位

    //如果上一輪循環有元素交換則繼續交換
    while(flag) {
        flag = false;
        left = (idx << 1);//左子節點
        right = left + 1;//右子節點

        if (left > heap[0])//無子節點
            break; 
        else if (right > heap[0])//只有左子節點
            minIdx = left;
        else
            minIdx = (heap[left] < heap[right]) ? left : right;

        if (heap[idx] > heap[minIdx]) {
            swap(heap, idx, minIdx);
            idx = minIdx;
            flag = true;//本次循環有元素交換
        }
    } 
}

//末位葉子節點“上浮”
private void goUp(int[] heap) {
    int idx = heap[0];
    int parent = (idx >> 1);

    while(parent > 0 && heap[idx] < heap[parent]) {
        swap(heap, idx, parent);
        idx  = parent;
        parent = (idx >> 1);
    }
}

希爾排序

/**
 * 希爾排序(縮小增量排序)
 * 
 * 原理:
 * 先設置一個較大的步長(增量),將數組分爲若干個子序列,對每個子序列分別進行排序,
 * 再減少步長,再次將數組分爲若干個更長的子序列,對每個子序列分別進行排序,
 * 如此循環,直到步長爲1,即整個數組中只有一個子序列,此時整個數組已經有序
 * 其中,子序列的排序可以採用任何其他排序算法,每一輪排序之後,數組將變得更加有序一些
 * 
 * @author Roamer
 */
void shellSort(int[] a) {
    //得到初始步長
    int step = 1;
    while(step < a.length) 
        step = 3*step + 1;

    while(step > 1) {
        //縮小步長
        step = step / 3 + 1;
        for (int i = 0; i < step; ++i) {
            // 得到子序列數組
            int nsub = (a.length - i - 1) / step + 1;
            int[] sub = new int[nsub];
            for (int j = 0; j < nsub; ++j)
                sub[j] = a[i + j * step];

            //對子序列數組進行冒泡排序
            bubbleSort(sub);

            //將排序後的元素保存到原數組的對應位置
            for (int j = 0; j < nsub; j++)
                a[i + j * step] = sub[j];
        }   
    }
}

基數排序

/**
 * 基數排序
 * 
 * 原理:
 * 基數排數基於桶排序的思想。如果需要排序的元素是正整數,則可以通過依次比較他們每個數位上
 * 數字的大小進行排序,由於十進制只有10個數碼,所以只需要10個“桶”就夠了。
 * 基數排序的方式可以採用LSD(Least sgnificant digital)或MSD(Most sgnificant digital),
 * 如果是採用LSD算法,則從個位開始,將所有整數根據個位數字分別放到對應的10個桶中,
 * 再按順序從各個桶中將所有整數取出依次填回原數組,然後將所有整數根據十位數字重新放到新的10個桶中,
 * 以此類推,直到所有元素的所有數位都遍歷完,由於基數排序是穩定的,所以數組中的所有元素最終都有序了
 * 
 * @author Roamer
 */
void radixSort(int[] a, int len) {
    int k = 0;//用於遍歷數組的下標指針
    int m = 1;//表示當前用於比較的數位,從個位開始
    int n = 1;//表示數位m對應的權重,即1,10,100,1000...

    //數組的第一維爲每個數位上可能出現的數字(0~9),即桶的個數,
    //第二維是包含當前數位的元素可能的總個數,即每個桶中可以放入的整數個數,
    //數組的元素值記錄了數位是lsd的整數
    int[][] bucket = new int[10][a.length];

    int[] order = new int[10];//用於記錄每個桶中整數的總個數 

    while(m <= len) {
        //入桶,即將數組中的所有整數按照數位m放到對應的桶中
        for(int i = 0; i < a.length; ++i) {     
            int lsd = (a[i] / n) % 10;//通過取整取餘得到數位m上的數字lsd
            bucket[lsd][order[lsd]] = a[i];//記錄新出現的數位是lsd的整數
            order[lsd]++;//數位是lsd的整數的個數加1
        }


        //出桶,即從10個桶中依次取出整數,填回數組中(即記錄當前已排好的相對順序)
        for(int i = 0; i < 10; i++) {
            //如果當前桶不爲空
            if(order[i] != 0) {
                //依次取出當前桶中的記錄的整數(bucket數組第二維中元素必定是連續的)
                for(int j = 0; j < order[i]; j++)
                    a[k++] = bucket[i][j];

                //清空對桶中整數個數的記錄 
                order[i] = 0;
            }
        }

        //完成當前數位上的排序,準備下一輪排序
        k = 0;//將數組a的下標重置爲0
        m++;//數位往高位增加
        n *= 10;//數位對應的權重增加
    }
}

參考資料

1: 紙上談兵: 排序算法簡介及其C實現

2: 面試中的排序算法總結

3: 基數排序——百度百科

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章