面試秘籍之排序算法

一、冒泡排序

排序思想:列表每兩個相鄰的數(shù)進行比較,如果前面的數(shù)比后面的數(shù)大,則交換這兩個數(shù),一輪排序完成后,則無序區(qū)減少一個數(shù),有序區(qū)增加一個數(shù)。

function sort(list) {
    for (let i= 0; len = list.length-1; i<len-1; i++) {
        let exchange = false; // 用來標記在一輪冒泡過程中有無交換過
        for (let j= 0; j<len-i; j++) {
            if (list[j] > list[j+1]) {
                // 交換兩個數(shù)
                const m = list[j];
                list[j] = list[j+1];
                list[j+1] = m;
                exchange = true;
            }
        }
        // 如果在一輪冒泡過程中沒有交換過,說明此時的列表已經是排序好的了,直接結束循環(huán)
        if (!exchange) {
            return;
        }
    }
}

二、選擇排序

排序思想:剛開始將整個數(shù)組看作一個無序區(qū),每一輪拿無序區(qū)的第一個數(shù)與無序區(qū)其他數(shù)值依次進行比較,遇到更小的數(shù)則交換,每一輪排序完成后有序區(qū)增加一個數(shù),無序區(qū)減少一個數(shù)。

function sort (list) {
    for (let i=0,len=list.length; i<len-1; i++) {
        for (let j=i+1; j<len; j++) {
            if (list[i] > list[j]) {
                // 交換兩個數(shù)
                const m = list[i];
                list[i] = list[j];
                list[j] = m;
            }
        }
    }
}

三、插入排序

排序思想:剛開始將整個數(shù)組看作一個無序區(qū),每一輪拿無序區(qū)的第一數(shù)與有序區(qū)的數(shù)從后往前依次進行比較,遇到更大的數(shù)則交換,每一輪排序完成后,有序區(qū)增加一個數(shù),無序區(qū)減少一個數(shù)。

function sort(list) {
    for (let i=1,len=list.length; i<len; i++) {
        for (let j=i-1; j>=0; j--) {
            if (list[j+1] < list[j]) {
                // 交換兩個數(shù)
                const m = list[j+1];
                list[j+1] = list[j];
                list[j] = m;
            }
        }
    }
}

四、快速排序

排序思想:取一個元素p(第一個數(shù)),使元素p歸位,此時列表會被p分成兩部分,左邊都比p小,右邊都比p大,然后左邊和右邊再分別進行遞歸使元素歸位,最終完成排序。

function sort(list, left, right) {
    if (left < right) {
        const mid = homing(list, left, right);
        sort(list, left, mid-1);
        sort(list, mid+1, right);
    }
}

/**
* 歸位函數(shù)
* 首先將第一個元素緩存起來,左指針剛開始指向第一個元素
* 然后右指針依次往左走,直到找到比緩存起來的元素小的元素,將該元素賦值給當前左指針指向的元素
* 然后左指針依次往右走,直到找到比緩存起來的元素大的元素,將該元素賦值給當前右指針指向的元素
* 重復以上操作,當左指針和右指針重合時,便找到了要歸位的地方,將緩存起來的元素賦值給左指針指向的元素,完成歸位
*/
function homing(li, left, right) {
    const tmp = li[left];
    while (left < right) {
        while (left < right && li[right] >= tmp) {
            right--;
        }
        li[left] = li[right];
        while(left < right && li[left] <= tmp) {
            left++;
        }
        li[right] = li[left];
    }
    li[left] = tmp;
    return left;
}

五、歸并排序

排序思想:假設有兩個排序好的數(shù)組,如何將他們合并成一個排序好的數(shù)組,首先創(chuàng)建一個空的新數(shù)組,將兩個有序的數(shù)組依次進行比較,每次將較小的數(shù)放到新數(shù)組中,最終得到一個排序好的新數(shù)組,此過程稱為歸并。數(shù)組為空或者只有一個元素時就是有序的,就可以進行歸并,歸并排序就是先兩兩進行歸并,再利用遞歸,將歸并后的兩個數(shù)組再進行歸并,最終得到排序好的數(shù)組。

function sort(list) {
    if (list && list.length > 1) {
        const mid = Math.floor(list.length / 2);
        const left = list.slice(0, mid);
        const right = list.slice(mid);
        return merge(sort(left), sort(right));
    }
    return list;
}

/**
* 歸并函數(shù)
* 首先創(chuàng)建一個空的新數(shù)組
* 將兩個有序數(shù)組中的元素依次進行比較,每次將較小的數(shù)放到新數(shù)組中
* 最終得到一個排序好的新數(shù)組
*/
function merge(leftList, rightList) {
    const newList = [];
    const leftLength = leftList && leftList.length;
    const rightLength = rightList && rightList.length;
    let i = 0;
    let j = 0;
    while (i < leftLength && j < rightLength) {
        if (leftList[i] < rightList[j]) {
            newList.push(leftList[i++]);
        } else {
            newList.push(rightList[j++]);
        }
    }
    while (i < leftLength) {
        newList.push(leftList[i++]);
    }
    while (j < rightLength) {
        newList.push(rightList[j++]);
    }
    return newList;
}

六、堆排序

排序思想:二叉樹的存存儲方式分為鏈式存儲和順序存儲,這里堆排序使用順序存儲。堆是一種特殊的完全二叉樹,堆分為大根堆和小根堆,滿足任一節(jié)點都比其孩子節(jié)點大的一個完全二叉樹就是大根堆,滿足任一節(jié)點都比其孩子節(jié)點小的一個完全二叉樹就是小根堆,這里堆排序使用大根堆。假設根節(jié)點的左右子樹都是堆,但是根節(jié)點不滿足堆的性質,可以通過一次向下調整來將其變成一個堆,這就是堆的向下調整性質。堆排序的排序過程是,首先構造一個大根堆(此時整個堆是無序區(qū)),然后將堆頂?shù)脑厝〕龇诺接行騾^(qū)(也就是數(shù)組的最后),然后將堆的最后一個元素(也就是無序區(qū)的最后一個元素)放到堆頂,堆就少了一個元素,此時通過一次向下調整重新使堆有序,調整后的堆頂就是整個數(shù)組的第二大元素,然后重復之前的操作依次將元素放到有序區(qū),直到堆變空,便可得到排序好的數(shù)組。

function sort(list) {
    if (list && list.length > 1) {
        const len = list.length;
        // 首先構造大根堆,從最后一個不是葉子節(jié)點的節(jié)點開始遍歷,從后往前依次進行向下調整
        for (let i = Math.floor((len-2)/2); i>=0; i--) {
            sift(list, i, len-1);
        }
        // 然后將堆的第一元素與有序區(qū)的第一個元素進行交換,此時有序區(qū)增加一個,無序區(qū)減少一個,再進行一次堆的向下調整,然后重復上述操作,最終使整個數(shù)組有序
        for(let i = len-1; i>=0; i--){
            const m = list[0];
            list[0] = list[i];
            list[i] = m;
            sift(list, 0, i-1);
        } 
    }
}

/**
* 堆的向下調整
* 先從根節(jié)點開始,如果孩子節(jié)點比父節(jié)點大,則將該孩子節(jié)點賦值給父節(jié)點
* 然后指針指向下一層,重復上面的操作,直到找到孩子節(jié)點沒有比父節(jié)點大的節(jié)點,終止循環(huán)
* 最后將原始的根節(jié)點賦值給當前父節(jié)點
*/
function sift(li, low, high) {
    const tmp = li[low]; // 緩存根節(jié)點
    let i = low; // 當前的父節(jié)點,最開始指向根節(jié)點
    let j = i*2+1; // 當前的孩子節(jié)點,最開始指向根節(jié)點的左孩子節(jié)點

    while (j <= high) {
        // 如果有右孩子節(jié)點且比左孩子節(jié)點大,則j指向右孩子節(jié)點
        if (j+1 <= high && li[j+1] > li[j]) {
            j++;
        }
        if (li[j] > tmp) {
            li[i] = li[j]; // 將較大的孩子節(jié)點賦值給父節(jié)點
            i = j; // i指向下一層
            j = i*2 +1;
        } else {
            break; // 如果當前子節(jié)點沒有比原始根節(jié)點大,結束循環(huán)
        }
    }

    li[i] = tmp; // 最后將原始的根節(jié)點賦值給當前父節(jié)點
}

總結

排序算法復雜度總結圖.png

更多個人文章

  1. 兩個跨域頁面進行跳轉傳參的終極方案
  2. 深入理解Event Loop的運行機制
  3. hashHistory和browserHistory的區(qū)別
  4. 面試秘籍之手寫系列
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容