js資料結構和演算法(9)-排序演算法

郭邯發表於2019-04-22

9.排序演算法

對計算機中儲存的資料執行的兩種最常見操作是排序和檢索,對計算機中儲存的資料執行的兩種最常見操作是排序和檢索。

9.1 輔助函式

在研究演算法前,我們先建立一個輔助函式,該函式的作用就是方便對我們的演算法進行測試,避免我們老是做一些重複性的工作。

class cArray {
  constructor(numElements) {
    this.dataStore = new Array(numElements)
    this.init()
  }
  // 初始化陣列
  init() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = i
    }
  }
  // 把陣列設定成隨機陣列
  setData() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = Math.floor(Math.random() * (this.dataStore.length + 1))
    }
  }
  // 清空陣列
  clear() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = 0
    }
  }
  // 在陣列頂部插入元素
  insert(ele) {
    this.dataStore.push(ele)
  }
  // 將陣列轉成字串
  toString() {
    let result = ''
    for (let i = 0; i < this.dataStore.length; i++) {
      result += this.dataStore[i] + ' '
      if (i > 0 && i % 10 == 9) {
        result += '\n'
      }
    }
    return result
  }
  // 交換陣列中的元素
  swap(index1, index2) {
    let temp = this.dataStore[index1]
    this.dataStore[index1] = this.dataStore[index2]
    this.dataStore[index2] = temp
  }
}
複製程式碼

測試:

// 生成工具函式物件
let cArr = new cArray(100)
// 設定隨機資料
cArr.setData()

console.log(cArr.toString())

// 列印結果
23 53 33 19 46 13 32 91 99 32 
50 57 88 4 65 46 61 77 81 3 
55 74 62 31 56 79 17 14 40 11 
65 92 93 71 80 92 37 22 92 83 
52 7 47 3 6 65 48 69 37 23 
64 43 14 76 92 4 4 20 49 96 
3 35 53 24 45 52 50 3 94 35 
43 50 68 37 14 9 75 93 97 57 
60 19 30 23 26 24 12 53 78 88 
65 28 25 68 95 8 0 35 5 10

// 清空陣列
cArr.clear()
console.log(cArr.toString())
// 列印結果
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 0 0 0 

//  交換陣列的元素
let dArr = new cArray(3)

console.log(dArr.toString())

// 列印結果
0 1 2 

dArr.swap(0, 1)

console.log(dArr.toString())

// 列印結果
1 0 2 
複製程式碼

我們的輔助函式能夠生成一個隨機陣列,

9.2 氣泡排序

氣泡排序演算法是最慢的排序演算法之一,但也是最容易實現的一種排序演算法。之所以叫氣泡排序是因為使用這種排序演算法排序時,資料值會像氣泡一樣從陣列的一端漂浮到另一端。假設正在將一組數字按照升序排列,較大的值會浮動到陣列的右側,而較小的值則會浮動到陣列的左側。之所以會產生這種現象是因為演算法會多次在陣列中移動,比較相鄰的資料,當左側值大於右側值時將它們進行互換 。

下圖演示瞭如何對一個大的數字資料集合進行氣泡排序。在圖中,我們分析了插入陣列中的兩個特定值: 2 和 72。這兩個數字都被圈了起來。你可以看到 72 是如何從陣列的開頭移動到中間的,還有 2 是如何從陣列的後半部分移動到開頭的。

程式:

bubbleSort() {
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let i = 0; i < len - 1; i++) {
      for (let j = len - 1; j > i; j--) {
        if (dataStore[j] < dataStore[j - 1]) {
          this.swap(j, j - 1)
        }
      }
    }
  }
複製程式碼

測試:

// 生成工具函式物件
let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()

console.log(cArr.toString())
//  5 3 9 4 2 6 10 5 3 8 

cArr.bubbleSort()

console.log(cArr.toString())
// 2 3 3 4 5 5 6 8 9 10 
複製程式碼

測試結果正確,我們的程式應該沒有問題。

9.3 選擇排序

我們接下來要看的是選擇排序演算法。選擇排序從陣列的開頭開始,將第一個元素和其他元素進行比較。檢查完所有元素後,最小的元素會被放到陣列的第一個位置,然後演算法會從第二個位置繼續。這個過程一直進行,當進行到陣列的倒數第二個位置時,所有的資料便完成了排序 。

程式實現:

  // 選擇排序
  selectionSort() {
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let i = 0; i < len - 1; i++) {
      for (let j = i + 1; j < len; j++) {
        if (dataStore[i] > dataStore[j]) {
          this.swap(i, j)
        }
      }
    }
  }
複製程式碼

測試

// 生成工具函式物件
let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()

console.log(cArr.toString())
// 3 2 0 10 7 4 4 7 0 6

cArr.selectionSort()

console.log(cArr.toString())
// 0 0 2 3 4 4 6 7 7 10 
複製程式碼

測試結果正確

9.4 插入排序

插入排序類似於人類按數字或字母順序對資料進行排序,插入排序有兩個迴圈。外迴圈將陣列元素挨個移動,而內迴圈則對外迴圈中選中的元素及它前面的所有元素進行比較。如果外迴圈中選中的元素比內迴圈中選中的元素小,那麼陣列元素會向右移動,然後在適當的位置插入外迴圈選中的元素。

以上的話,特別繞,還是直接看程式清晰

程式碼實現:

  // 插入排序
  insertionSort() {
    let temp, inser
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let outer = 1; outer < len; outer++) {
      temp = dataStore[outer]
      let inner = outer
      while (inner > 0 && dataStore[inner - 1] > temp) {
        this.swap(inner, inner - 1)
        inner--
      }
      dataStore[inner] = temp
    }
  }
複製程式碼

測試

// 生成工具函式物件
let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()

console.log(cArr.toString())
// 4 9 8 8 3 8 5 8 5 0 

cArr.insertionSort()

console.log(cArr.toString())
// 0 3 4 5 5 8 8 8 8 9 
複製程式碼

測試結果正確,說明我們的程式沒有問題。

9.4 演算法時間比較

測試:

// 生成工具函式物件
let cArr = new cArray(1000)
// 設定隨機資料
cArr.setData()
// 儲存生成的隨機陣列,為了比較各個演算法的時間複雜度,必須保證它們處理的隨機陣列是一樣的
let dataStore = cArr.dataStore.concat()
// 氣泡排序
console.time('bubbleSort')
cArr.insertionSort()
console.timeEnd('bubbleSort')

// 選擇排序
cArr.dataStore = dataStore.concat()
console.time('selectionSort')
cArr.insertionSort()
console.timeEnd('selectionSort')

// 插入排序
cArr.dataStore = dataStore.concat()
console.time('insertion')
cArr.insertionSort()
console.timeEnd('insertion')
複製程式碼

測試結果:

bubbleSort: 5.827880859375ms
selectionSort: 3.77392578125ms
insertion: 0.712890625ms
複製程式碼

處理同樣的1000條資料,我們可以看到氣泡排序是最慢的,其次是選擇排序,插入排序是最快的。

不過這三種排序方法,在大資料時,效能還是較慢。以下是更加高階的演算法。這演算法在效率上要更高。

9.5 希爾排序

這個演算法在插入排序的基礎上做了很大的改善。希爾排序的核心理念與插入排序不同,它會首先比較距離較遠的元素,而非相鄰的元素。和簡單地比較相鄰元素相比,使用這種方案可以使離正確位置很遠的元素更快地回到合適的位置。當開始用這個演算法遍歷資料集時,所有元素之間的距離會不斷減小,直到處理到資料集的末尾,這時演算法比較的就是相鄰元素了 。

希爾排序的工作原理是,通過定義一個間隔序列來表示在排序過程中進行比較的元素之間有多遠的間隔。我們可以動態定義間隔序列,不過對於大部分的實際應用場景,演算法要用到的間隔序列可以提前定義好。有一些公開定義的間隔序列可以給我們使用。這個間隔序列是: 701, 301, 132, 57, 23, 10, 4, 1

下圖演示了希爾排序的工作原理

希爾排序程式碼

 // 希爾排序
  shellSort () {
    // 間隔序列
    let gaps = [5, 3, 1]
    for (let g = 0; g < gaps.length; g++) {
      let tempG = gaps[g]
      for (let i = tempG; i < this.dataStore.length; i++) {
        let temp = this.dataStore[i]
        let j = i
        while (j >= tempG && this.dataStore[j - tempG] > temp) {
          this.dataStore[j] = this.dataStore[j - tempG]
          j -= tempG
        }
        this.dataStore[j] = temp
      }
    }
  }
複製程式碼

測試

// 生成工具函式物件
let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()
console.log(cArr.toString()) 
// 4 7 8 10 1 1 0 3 10 4 
cArr.shellSort()
console.log(cArr.toString())
// 0 1 1 3 4 4 7 8 10 10 
複製程式碼

測試結果正確,說明我們的程式沒有問題。

9.5.1 計算動態間隔序列

希爾排序的間隔序列是可以動態計算的,計算公式如下。

let N = this.dataStore.length;
let h = 1;
while (h < N/3) {
h = 3 * h + 1;
}
複製程式碼

間隔值確定好後,這個函式就可以像之前定義的 shellsort() 函式一樣執行了,唯一的區別是,回到外迴圈之前的最後一條語句會計算一個新的間隔值。

h = (h-1)/3
複製程式碼

對我們的shell排序進行重新優化

  // 希爾排序
  shellSort () {
    // 間隔序列
    let N = this.dataStore.length
    let h = 1
    while (h < N / 3) {
      h = 3 * h + 1
    }
    while (h >= 1) {
      for (let i = h; i < this.dataStore.length; i++) {
        let temp = this.dataStore[i]
        let j = i
        while (j >= h && this.dataStore[j - h] > temp) {
          this.dataStore[j] = this.dataStore[j - h]
          this.swap(j, j - h)
          j -= h
        }
        this.dataStore[j] = temp
      }
      h = (h - 1) / 3
    }
  }
複製程式碼

測試是正確的,我就不貼測試結果了。關於希爾排序的知識就這些。

9.6 歸併排序

歸併排序的命名來自它的實現原理:把一系列排好序的子序列合併成一個大的完整有序序列。從理論上講,這個演算法很容易實現。我們需要兩個排好序的子陣列,然後通過比較資料大小,先從最小的資料開始插入,最後合併得到第三個陣列。

歸併排序的核心思想是分治,分治是通過遞迴地將問題分解成相同或者型別相關的兩個或者多個子問題,直到問題簡單到足以解決,然後將子問題的解決方案結合起來,解決原始方案的一種思想。

歸併排序通過將複雜的陣列分解成足夠小的陣列(只包含一個元素),然後通過合併兩個有序陣列(單元素陣列可認為是有序陣列)來達到綜合子問題解決方案的目的。所以歸併排序的核心在於如何整合兩個有序陣列,拆分陣列只是一個輔助過程。

作者寫的這個歸併排序演算法非常複雜。我跟著捋了幾遍,才略懂,有更好的講解的話請@我。

歸併排序程式碼:

  mergeSort() {
    let arr = this.dataStore
    if (arr.length < 2) {
      return
    }
    let step = 1
    let left, right
    while (step < arr.length) {
      left = 0;
      right = step
      while (right + step <= arr.length) {
        this.mergeArrays(arr, left, left + step, right, right + step)
        left = right + step
        right = left + step
      }
      if (right < arr.length) {
        this.mergeArrays(arr, left, left + step, right, arr.length)
      }
      step *= 2;

    }
  }
  mergeArrays(arr, startLeft, stopLeft, startRight, stopRight) {
    let rightArr = new Array(stopRight - startRight + 1)
    let leftArr = new Array(stopLeft - startLeft + 1)
    let k = startRight
    for (let i = 0; i < (rightArr.length - 1); i++) {
      rightArr[i] = arr[k]
      k++
    }
    k = startLeft
    for (let i = 0; i < (leftArr.length - 1); i++) {
      leftArr[i] = arr[k]
      k++
    }
    rightArr[rightArr.length - 1] = Infinity
    leftArr[leftArr.length - 1] = Infinity
    let m = 0;
    let n = 0;
    for (let k = startLeft; k < stopRight; ++k) {
      if (leftArr[m] < rightArr[n]) {
        arr[k] = leftArr[m]
        m++
      } else {
        arr[k] = rightArr[n]
        n++
      }
    }
  }
複製程式碼

測試:

let cArr = new cArray(10)
cArr.setData()
console.log(cArr.toString())
console.time('mergeSort')
// 5 7 5 9 8 7 8 5 2 5 
cArr.mergeSort()
console.timeEnd('mergeSort')
console.log(cArr.toString())
// 2 5 5 5 5 7 7 8 8 9 
複製程式碼

9.7 快速排序

快速排序是處理大資料集最快的排序演算法之一。它是一種分而治之的演算法,通過遞迴的方式將資料依次分解為包含較小元素和較大元素的不同子序列。該演算法不斷重複這個步驟直到所有資料都是有序的。

這個演算法首先要在列表中選擇一個元素作為基準值(pivot)。資料排序圍繞基準值進行,將列表中小於基準值的元素移到陣列的底部,將大於基準值的元素移到陣列的頂部。

程式

// 快速排序
  qSort() {
    this.dataStore = this.qSortArr(this.dataStore)
  }
  qSortArr(list) {
    if (list.length == 0) {
      return []
    }
    let lesser = []
    let greater = []
    let pivot = list[0]
    for (let i = 1; i < list.length; i++) {
      if (list[i] < pivot) {
        lesser.push(list[i])
      } else {
        greater.push(list[i])
      }
    }
    return this.qSortArr(lesser).concat(pivot, this.qSortArr(greater))
  }
複製程式碼

測試

let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()
console.log(cArr.toString())
// 8 7 2 9 10 2 3 5 9 4 
console.time('qSort')
cArr.qSort()
console.timeEnd('qSort')
console.log(cArr.toString())
// 2 2 3 4 5 7 8 9 9 10 
複製程式碼

自此,關於排序的演算法就講解忘了,讀這一章用了很多時間,排序確實比較難,尤其是那個歸併排序。好難理解。自己嘗試實現,結果沒實現出來,如果有好的實現方法,請一定告訴我。

本章完整的程式碼如下:

class cArray {
  constructor(numElements) {
    this.dataStore = new Array(numElements)
    this.init()
  }
  // 初始化陣列
  init() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = i
    }
  }
  // 把陣列設定成隨機陣列
  setData() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = Math.floor(
        Math.random() * (this.dataStore.length + 1)
      )
    }
  }
  // 清空陣列
  clear() {
    for (let i = 0; i < this.dataStore.length; i++) {
      this.dataStore[i] = 0
    }
  }
  // 在陣列頂部插入元素
  insert(ele) {
    this.dataStore.push(ele)
  }
  // 將陣列轉成字串
  toString() {
    let result = ''
    for (let i = 0; i < this.dataStore.length; i++) {
      result += this.dataStore[i] + ' '
      if (i > 0 && i % 10 == 9) {
        result += '\n'
      }
    }
    return result
  }
  // 交換陣列中的元素
  swap(index1, index2) {
    let temp = this.dataStore[index1]
    this.dataStore[index1] = this.dataStore[index2]
    this.dataStore[index2] = temp
  }
  // 氣泡排序
  bubbleSort() {
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let i = 0; i < len - 1; i++) {
      for (let j = len - 1; j > i; j--) {
        if (dataStore[j] < dataStore[j - 1]) {
          this.swap(j, j - 1)
        }
      }
    }
  }
  // 選擇排序
  selectionSort() {
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let i = 0; i < len - 1; i++) {
      for (let j = i + 1; j < len; j++) {
        if (dataStore[i] > dataStore[j]) {
          this.swap(i, j)
        }
      }
    }
  }
  // 插入排序
  insertionSort() {
    let temp, inser
    let dataStore = this.dataStore
    let len = dataStore.length
    for (let outer = 1; outer < len; outer++) {
      temp = dataStore[outer]
      let inner = outer
      while (inner > 0 && dataStore[inner - 1] > temp) {
        this.swap(inner, inner - 1)
        inner--
      }
      dataStore[inner] = temp
    }
  }

  // 希爾排序
  shellSort() {
    // 間隔序列
    let N = this.dataStore.length
    let h = 1
    while (h < N / 3) {
      h = 3 * h + 1
    }
    while (h >= 1) {
      for (let i = h; i < this.dataStore.length; i++) {
        let temp = this.dataStore[i]
        let j = i
        while (j >= h && this.dataStore[j - h] > temp) {
          this.dataStore[j] = this.dataStore[j - h]
          this.swap(j, j - h)
          j -= h
        }
        this.dataStore[j] = temp
      }
      h = (h - 1) / 3
    }
  }
  // 歸併排序
  mergeSort() {
    let arr = this.dataStore
    if (arr.length < 2) {
      return
    }
    let step = 1
    let left, right
    while (step < arr.length) {
      left = 0;
      right = step
      while (right + step <= arr.length) {
        this.mergeArrays(arr, left, left + step, right, right + step)
        left = right + step
        right = left + step
      }
      if (right < arr.length) {
        this.mergeArrays(arr, left, left + step, right, arr.length)
      }
      step *= 2;

    }
  }
  mergeArrays(arr, startLeft, stopLeft, startRight, stopRight) {
    let rightArr = new Array(stopRight - startRight + 1)
    let leftArr = new Array(stopLeft - startLeft + 1)
    let k = startRight
    for (let i = 0; i < (rightArr.length - 1); i++) {
      rightArr[i] = arr[k]
      k++
    }
    k = startLeft
    for (let i = 0; i < (leftArr.length - 1); i++) {
      leftArr[i] = arr[k]
      k++
    }
    rightArr[rightArr.length - 1] = Infinity
    leftArr[leftArr.length - 1] = Infinity
    let m = 0;
    let n = 0;
    for (let k = startLeft; k < stopRight; ++k) {
      if (leftArr[m] < rightArr[n]) {
        arr[k] = leftArr[m]
        m++
      } else {
        arr[k] = rightArr[n]
        n++
      }
    }
  }
  // 快速排序
  qSort() {
    this.dataStore = this.qSortArr(this.dataStore)
  }
  qSortArr(list) {
    if (list.length == 0) {
      return []
    }
    let lesser = []
    let greater = []
    let pivot = list[0]
    for (let i = 1; i < list.length; i++) {
      if (list[i] < pivot) {
        lesser.push(list[i])
      } else {
        greater.push(list[i])
      }
    }
    return this.qSortArr(lesser).concat(pivot, this.qSortArr(greater))
  }
}
// 生成工具函式物件
let cArr = new cArray(10)
// 設定隨機資料
cArr.setData()

// 儲存生成的隨機陣列,為了比較各個演算法的時間複雜度,必須保證它們處理的隨機陣列是一樣的
let dataStore = cArr.dataStore.concat()
// 氣泡排序
console.time('bubbleSort')
cArr.insertionSort()
console.timeEnd('bubbleSort')

// 選擇排序
cArr.dataStore = dataStore.concat()
console.time('selectionSort')
cArr.insertionSort()
console.timeEnd('selectionSort')

// 插入排序
cArr.dataStore = dataStore.concat()
console.time('insertion')
cArr.insertionSort()
console.timeEnd('insertion')

// 希爾排序
cArr.dataStore = dataStore.concat()
console.time('shell')
cArr.shellSort()
console.timeEnd('shell')

// 歸併排序
cArr.dataStore = dataStore.concat()
console.time('mergeSort')
cArr.mergeSort()
console.timeEnd('mergeSort')


cArr.dataStore = dataStore.concat()
console.time('qSort')
cArr.qSort()
console.timeEnd('qSort')
複製程式碼

相關文章