排序算法

排序算法

直接插入排序

直接插入排序的思路是遍歷過程中保持被遍歷過的元素有序,每遍歷到一個新元素時如不能保持已遍歷元素有序,則爲該新元素尋找合適的插入位置插入以保持遍歷元素的有序性,當完成遍歷時所有元素均有序。

假設初始列表items = [4, 2, 3, 1, 5],非降序的直接插入排序過程如下:

  1. 初始已遍歷內容爲[4],從i = 1開始,items[i = 1] = 2,[4] => 插入2 => [2, 4]
  2. items[i = 2] = 3,[2, 4] => 插入3 => [2, 3, 4]
  3. items[i = 3] = 1,[2, 3, 4] => 插入1 => [1, 2, 3, 4]
  4. items[i = 4] = 5,[1, 2, 3, 4] => 插入5 => [1, 2, 3, 4, 5]

代碼實現:

function insertSort(items) {
    let i, j, tmp;

    for (i = 1; i < items.length; i++) {
        if (items[i] < items[i - 1]) {
            tmp = items[i];
            items[i] = items[i - 1];

            for (j = i - 1; j >= 0 && items[j] > tmp; j--) {
                items[j + 1] = items[j];
            }

            items[j + 1] = tmp;
        }
    }

    return items;
}

冒泡排序

經典排序算法,可總結成:“兩兩比較,兩兩交換”,每一輪比較和交換可以將一個最大或最小元素移動到對應的位置。

假設初始列表items = [4, 2, 3, 1, 5],非降序的冒泡排序過程如下:

  1. [(2, 4), 3, 1, 5] => [2, (3, 4), 1, 5] => [2, 3, (1, 4), 5] => [2, 3, 1, (4, 5)]
  2. [(2, 3), 1, 4, 5] => [2, (1, 3), 4, 5] => [2, 1, (3, 4), 5]
  3. [(1, 2), 3, 4, 5] => [1, (2, 3), 4, 5]
  4. [(1, 2), 3, 4, 5] => [1, 2, 3, 4, 5]

流程裏中括號內的小括號是進行比較和交換的元素,整個排序看起來像是目標元素從列表中向上“冒出”的過程,所以被稱爲冒泡排序。

代碼實現:

function bubbleSort(items) {
    let i, j, tmp;

    for (i = 0; i < items.length - 1; i++) {
        for (j = 1; j < items.length - i; j++) {
            if (items[j] < items[j - 1]) {
                tmp = items[j];
                items[j] = items[j - 1];
                items[j - 1] = tmp;
            }
        }
    }

    return items;
}

選擇排序

選擇排序的思路是在遍歷過程中依次挑選出最大、次大、第三大…元素,並將其移動到對應的位置上,當然也可以反過來用挑選最小、次小、第三小…的方式。

假設初始列表items = [4, 2, 3, 1, 5],非降序的選擇排序過程如下:

  1. i = 0,找到最小元素的下標爲3,交換後:[1, 2, 3, 4, 5]
  2. i = 1,…,[1, 2, 3, 4, 5]
  3. i = 2,…,[1, 2, 3, 4, 5]
  4. i = 3,…,[1, 2, 3, 4, 5]

代碼實現:

function selectSort(items) {
    let i, j, tmp;

    for (i = 0; i < items.length - 1; i++) {
        let k = i;
        for (j = i + 1; j < items.length; j++) {
            if (items[k] > items[j]) {
                k = j;
            }
        }

        tmp = items[k];
        items[k] = items[i];
        items[i] = tmp;
    }

    return items;
}

希爾排序

希爾排序是對直接插入排序的一種優化改進,做法是先挑選一個遞減的delta序列,隨後遍歷delta序列時從待排序序列中按步長爲delta的方式挑選子序列(共delta個)進行直接插入排序,直到delta = 1進行最後一次排序後,整個序列有序。

假設初始列表items = [4, 2, 6, 3, 1, 5, 8, 7],非降序的希爾排序排序過程如下:

  1. 選擇delta序列[3, 2, 1]
  2. delta = 3,對子序列[4, 3, 8]、[2, 1, 7]、[6, 5]分別排序得到[3, 4, 8]、[1, 2, 7]、[5, 6],此時items = [3, 1, 5, 4, 2, 6, 8, 7]
  3. delta = 2,對子序列[3, 5, 2, 8]、[1, 4, 6, 7]分別排序得到[2, 3, 5, 8]、[1, 4, 6, 7],此時items = [2, 1, 3, 4, 5, 6, 8, 7]
  4. delta = 1,對序列[2, 1, 3, 4, 5, 6, 8, 7]排序得到[1, 2, 3, 4, 5, 6, 7, 8]

代碼實現:

function shellSort(items) {
    let delta = [];
    let n = items.length;
    do {
        delta.push(n = Math.floor(n / 2));
    } while (n > 1);

    let i, j, k, l, tmp;

    for (i = 0; i < delta.length; i++) {
        j = delta[i];

        for (k = j; k < items.length; k++) {
            if (items[k] < items[k - j]) {
                tmp = items[k];
                for (l = k - j; l >= 0 && tmp < items[l]; l -= j) {
                    items[l + j] = items[l];
                }
                items[l + j] = tmp;
            }
        }
    }

    return items;
}

快速排序

快速排序的思路是先從序列中選擇一個元素作爲“軸中數”,然後分別將大於軸中數的元素放到右側,小於的元素放在左側,接着對左側和右側的數再次採用快速排序,快速排序的終止條件是待排序序列長度不大於1。

假設初始列表items = [4, 2, 3, 1, 5],非降序的快速排序過程如下:

  1. [4, 2, 3, 1, 5]:4 => [(1, 2, 3), 4, (5)]
  2. [1, 2, 3]:1、[5]:5 => [1, (2, 3), 4, 5]
  3. [2, 3]:2 => [1, 2, (3), 4, 5]
  4. [3]:3 => [1, 2, 3, 4, 5]

上述過程,中括號接冒號後面的值表示所選的軸中數(序列第一個元素),代碼如下:

function quickSort(items) {
    return _sort(items, 0, items.length - 1);

    function _sort(items, low, high) {
        if (low >= high) {
            return items;
        }

        let i = low;
        let j = high;
        let pivot = items[low];

        while (i < j) {
            while (i < j && items[j] >= pivot) j--;
            items[i] = items[j];
            while (i < j && items[i] <= pivot) i++;
            items[j] = items[i];
        }
        items[i] = pivot;

        _sort(items, low, i - 1);
        _sort(items, i + 1, high);

        return items;
    }
}

堆排序

對於長度爲n的序列,大根堆的元素滿足:Ai >= A2i, 2i+1(2i + 1 < n),小根堆的元素滿足:Ai <= A2i, 2i+1(2i + 1 < n)。可將序列轉換爲一顆完全二叉樹,大根堆的約束是所有非葉子結點不小於其左右孩子結點,小根堆則是不大於。

堆排序過程是先將序列調整成大根堆或小根堆,然後取走堆的根元素並用最後面的元素填充到根元素位置,隨後反覆調整堆符合大(小)根堆的定義以及取走根元素與填充最後元素到根元素位置,直到堆爲空。

假設初始列表items = [4, 2, 6, 3, 7, 1, 5],他對應的完全二叉樹如下:

        |------4------|
        |             |
    |---2---|     |---6---|
    |       |     |       |
    3       7     1       5

非降序的堆排序過程如下:

  1. 從items[floor(items.length / 2) - 1] = items[2] = 6開始向前調整,使其逐漸成爲最大堆
  2. [4, 2, 6, 3, 7, 1, 5] => 6與1、5比較,不需要交換 => [4, 2, 6, 3, 7, 1, 5]
  3. [4, 2, 6, 3, 7, 1, 5] => 2與3、7比較,2與7交換 => [4, 7, 6, 3, 2, 1, 5]
  4. [4, 7, 6, 3, 2, 1, 5] => 4與7、6比較,4與7交換 => [7, 4, 6, 3, 2, 1, 5],每一次交換需要考慮交換後的元素作爲根的堆是否滿足大根堆定義,不滿足則需要繼續交換,這裏4與7交換後,4作爲根的堆左右孩子分別爲3、2,故無需調整

經過上面4步後,整個序列已經符合大根堆的定義,其對應的完全二叉樹如下:

        |------7------|
        |             |
    |---4---|     |---6---|
    |       |     |       |
    3       2     1       5

接着進行後續步驟:

  1. 取走7,將5放在7的位置,並調整5作爲根的堆成爲大根堆,[7, 4, 6, 3, 2, 1, 5] => [5, 4, 6, 3, 2, 1]、[7] => [6, 4, 5, 3, 2, 1]、[7]
  2. [6, 4, 5, 3, 2, 1]、[7] => 取走6用1替代 => [1, 4, 5, 3, 2]、[6, 7] => 調整1作爲根的堆成爲大根堆 => [5, 4, 1, 3, 2]、[6, 7]
  3. [5, 4, 1, 3, 2]、[6, 7] => 取走5用2替代 => [2, 4, 1, 3]、[5, 6, 7] => 調整2作爲根的堆成爲大根堆 => [4, 3, 1, 2]、[5, 6, 7]
  4. …與上類似
  5. 最終得到[]、[1, 2, 3, 4, 5, 6, 7]

代碼如下:

function heapSort(items) {
    let len = items.length;

    for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
        _fixHeap(items, i, len - 1);
    }

    for (let i = len - 1; i > 0; i--) {
        let tmp = items[0];
        items[0] = items[i];
        items[i] = tmp;

        _fixHeap(items, 0, i - 1);
    }

    return items;

    function _fixHeap(items, s, m) {
        let tmp = items[s];

        for (let i = 2 * s + 1; i <= m; i = i * 2 + 1) {
            if (i < m && items[i] < items[i + 1]) {
                i++;
            }

            if (tmp >= items[i]) {
                break;
            }

            items[s] = items[i];
            s = i;
        }

        items[s] = tmp;
    }
}

歸併排序

歸併排序的過程是多次將兩個有序序列合併爲一個有序序列,直到最後只剩下一個序列。

假設初始列表items = [4, 2, 6, 3, 1, 5, 8, 7],非降序的歸併排序排序過程如下(正向描述):

  1. [4, 2, 6, 3, 1, 5, 8, 7] => 8個有序序列兩兩合併 => [(2, 4), (3, 6), (1, 5), (7, 8)]
  2. [(2, 4), (3, 6), (1, 5), (7, 8)] => 4個有序序列兩兩合併 => [(2, 3, 4, 6), (1, 5, 7, 8)]
  3. [(2, 3, 4, 6), (1, 5, 7, 8)] => 2個有序序列合併 => [(1, 2, 3, 4, 5, 6, 7, 8)]

代碼採用的是遞歸方式,如下:

function mergeSort(items) {
    if (items.length <= 1) {
        return items;
    }

    let mid = Math.floor(items.length / 2);
    let left = mergeSort(items.slice(0, mid));
    let right = mergeSort(items.slice(mid));

    let result = [];
    let i = 0;
    let j = 0;
    while (i < left.length || j < right.length) {
        if (i === left.length) {
            result.push(right[j++]);
            continue;
        }
        if (j === right.length) {
            result.push(left[i++]);
            continue;
        }

        if (left[i] < right[j]) {
            result.push(left[i++]);
        }
        else {
            result.push(right[j++]);
        }
    }

    return result;
}

博客原文

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