07 Javascript資料結構與演算法 之 排序演算法

zhaoyezi發表於2018-09-03

1. 排序演算法

排序演算法就是講一組雜亂無章的資料,按照從小到大從大到小的方式,變成一組有序的資料。首先,我們編寫一個陣列函式,用於產生陣列和列印陣列。

function ArrayList() {
    let arr = [];

    // 插入資料
    this.insert = (item) => {
        arr.push(item);
    };

    // 列印資料
    this.toString = () => {
        return arr.join(',');
    };

    // 交換兩個元素的位置
    let swap = (index1, index2) => {
        let aux = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = aux;
    }
}
複製程式碼

2. 氣泡排序

冒泡是演算法中最簡單的,但是從時間上來看,是最慢的。

什麼是氣泡排序?

  • 氣泡排序就是 取出陣列中的第n個元素a,將該元素與 其後面所有的元素(b)依次比較,如果 a > b,交換它們的位置
  • 但在實際過程中,當a元素被放到最後,它就不應該再參與比較

07 Javascript資料結構與演算法 之  排序演算法

this.bubbleSort = () => {
    let length = arr.length;
    for (let j = 0; j < length; j++) {
        // 因為 i 要與 i + 1 比較,因此 i <length -1; 不然會越界
        for (let i = 0; i < length - 1 - j; i++) {
            if (arr[i] > arr[i+1]) {
                swap(i, i+1);
            }
        }
    }
};
複製程式碼

3. 選擇排序

選擇排序:先找到資料結構中最小的值,並將其放在第一位,接著找到資料中第二小的值,放在第二位,依次類推。

07 Javascript資料結構與演算法 之  排序演算法

this.selectSort = () => {
    let length = arr.length, min;
    for (let i = 0; i < length; i++) {
        // 將當前位置設定為min
        min = i;
        for (let j = i; j < length; j++) {
            if (arr[min] > arr[j]) {
                min = j;
            }
        }
        if ( i !== min) {
            swap(min, i);
        }
    }
}
複製程式碼

選擇排序同樣也是一個複雜度為O(n2)的演算法。和氣泡排序一樣,它包含有巢狀的兩個迴圈,這導致了二次方的複雜度。

4. 插入排序

插入排序是每次排一個陣列項。

  • 排第一個值,就保留在第一個位置
  • 排第二個值,看它放在第一個位置,還是第二個位置
  • 排第三個值,看它是放在第一個位置,還是第二個位置,還是第三個位置
  • ...

總結一句話:當計算a的正確位置,與比a索引小的值比較,將比a大的值全部往後移動,騰出位置,將a插入

07 Javascript資料結構與演算法 之  排序演算法

// for 迴圈做法
this.insertSort = () => {
    let length = arr.length;
    for (let i = 0; i < length; i++) {
        let insertIndex = i;
        for(let j = i-1; j >=0; j--) {
            if (arr[j] > arr[insertIndex]) {
                swap(j, insertIndex);
                insertIndex = j;
            } else {
                break;
            }
        }
    }
}

// while迴圈
 this.insertSort = () => {
    let length = arr.length,
        j;
    for (let i = 0; i < length; i++) {
        j = i, // 會改變,用於儲存當前需要對比的索引
        temp = arr[i]; // 儲存需要移動的value
        while(j > 0 && arr[j-1] > temp) {
            // 將大於temp的值往後移動,給temp騰出位置
            arr[j] = arr[j-1];
            j--;
        }
        // 將temp 賦值給騰出來的位置
        arr[j] = temp;
    }
}
複製程式碼

排序小型陣列時,此演算法比選擇排序和氣泡排序效能要好。

5. 歸併排序

歸併排序是一種分治演算法。將元是陣列切分成較小的陣列,直到每個小陣列只有一個位置,接著將小陣列歸併成為較大的陣列,直到最後,只有一個排序完畢的大陣列。
由於分治法:歸併排序也是遞迴

07 Javascript資料結構與演算法 之  排序演算法

let mergeSortRec = (array) => {
    let length = array.length;
    if(length === 1) { //{1}
        return array; //{2}
    }
    // 將陣列分為左右兩組
    let mid = Math.floor(length / 2),
    left = array.slice(0, mid),
    right = array.slice(mid, length);
    // 遞迴呼叫,將陣列拆分為只有一個長度的陣列,通過merge(排序)
    return merge(mergeSortRec(left), mergeSortRec(right));
}
let merge = (left, right) => {
    // 記錄 left的索引,right的索引
    let iL = 0, 
    iR = 0, 
    lLength = left.length, 
    rLength = right.length;
    let result = [];
    // 如果 left或right其中某個被迴圈處理完畢,跳出迴圈
    while(iL < lLength && iR < rLength) {
        if (left[iL] < right[iR]) {
            result.push(left.shift());
            iL++;
        } else {
            result.push(right.shift());
            iR++;
        }
    }
    // 將剩下的沒被處理的資料,追加到結果集中
    if (iL < left.length) {
        result.push(...left);
    }
    if (iR < right.length) {
        result.push(...right);
    }
    return result;
}
複製程式碼

歸併排序的效能比前三個好,是O(nlog^n)。

6. 快速排序

快速排序也是分治方法,將原始陣列分為較小的組(但並沒有像歸併排序那樣將它們分開)。

  • 找到陣列中間一項 作為主元
  • 建立兩個指標,指向陣列第一項和最後一項
  • 向右移動左指標,直到找到元素比主元大的值a,停止
  • 向左移動右指標,直到找到元素比主元小的值b,停止
  • 交換a,b兩個值,並將指標繼續移動
  • 當左指標 > 右指標,結束比較。將左指標作為下一次主元的索引,。
this.quickSort = () => {
    quick(arr, 0, arr.length - 1);
};

//  排序arr, 左指標, 右指標
let quick = (sortArr, left, right) => {
    if (sortArr.length <= 0) {
        return;
    } 
    // 排序: 最終生成一個陣列,按照步驟,以主元為基準,移動左右指標處理一遍後的資料
     let index = partition(sortArr, left, right);
    // 以主元為基準,左邊的再一次排序
    if (left < index - 1) {
        quick(sortArr, left, index - 1);
    } 
    // 以主元為基準,右邊的再一次排序
    if (index < right) {
        quick(sortArr, index, right);
    }
};
let partition = (sortArr, left, right) => {
    // 本輪迴圈的 主元
    let pivot = sortArr[Math.floor((left + right)/2)];
    // 左索引,右索引
    let lI = left, lR = right;
    // 當左索引 > 右索引時,結束本輪排序
    while( lI <= lR) {
        // 找到 主元 左邊 大於它的值
        while (sortArr[lI] < pivot) {
            lI++;
        }
        while(sortArr[lR] > pivot) {
            lR--;
        }
        if (lI <= lR) {
            swap(lI, lR);
            lI++;
            lR--;
        }
    }
    // 返回左索引,作為下一次分組的 `主元`
    return lI;
}
複製程式碼

快速排序步驟:

  • 在資料集之中,選擇一個元素作為"基準"(pivot)。
  • 所有小於"基準"的元素,都移到"基準"的左邊;所有大於"基準"的元素,都移到"基準"的右邊。
  • 對"基準"左邊和右邊的兩個子集,不斷重複第一步和第二步,直到所有子集只剩下一個元素為止。

快速排序的雖然也是O(nlog^n),但是效能比其他O(nlog^n)的好。

7 順序查詢

在一個陣列中查詢某一個具體值位置的實現

this.sequentialSearch = (item) => {
    for ( let i = 0; i < arr.length; i++) {
         if (arr[i] === item) {
             return i;
         }
    }
    return -1;
}
複製程式碼

8. 使用二分搜尋查詢方式

該演算法要求:被搜尋的資料結構是已排序結構。

  1. 選擇陣列的中間值
  2. 如果選中值是待搜尋值,返回結果
  3. 如果搜尋值比選中值小,則到第一步:並從選中值的左邊的子陣列中查詢
  4. 如果搜尋值比選中值大,則到第一步:並從選中值的右邊的子陣列中查詢
this.binarySearch = (item) {
    // 先排序
    this.quickSort();

    // 左邊開始索引
    let low = 0, 
    // 右邊開始索引
    high = arr.length -1, 
    // 中間索引
    mid, 
    // 當前選中被比較的值
    element;
    while (low < high) {
        mid = Math.floor((mid + high) / 2);
        element = arr[mid];
        // 代表被查詢值在mid的左邊
        if (element > item) {
            high = mid;

        // 代表被查詢的值在mid的右邊    
        } else if (element < item) {
            low = high;
        } else {
            return mid;
        }
    }
    return -1;
}
複製程式碼

相關文章