JS 實現 10 大經典排序算法

0. 算法複雜度

排序算法 時間複雜度(平均) 空間複雜度 穩定性
冒泡排序 O(n2) O(1) 穩定
快速排序 O(nlog2n) O(log2n) 不穩定
簡單插入排序 O(n2) O(1) 穩定
shell 排序 O(n1.3) O(1) 不穩定
簡單選擇排序 O(n2) O(1) 不穩定
堆排序 O(nlog2n) O(1) 不穩定
二路歸併排序 O(nlog2n) O(n) 穩定
計數排序 O(n + k) O(n + k) 穩定
桶排序 O(n + k) O(n + k) 穩定
基數排序 O(n * k) O(n + k) 穩定

TIP: 以下算法的說明與實現,均以升序排列爲例。

1. 冒泡排序

基本思想:

  1. 從數組第一個元素開始,重複比較前後相鄰的 2 個數組元素,如果前面一個元素大於後面一個元素,則交換 2 個元素的位置;
  2. 經過步驟 1 描述的一輪循環後,無序數列中的最大值將被放置在無序數列的末尾(此時,該值已有序)。至此,無序數列長度減 1;
  3. 對無序數列重複步驟 1、2,直至整個數列有序(無序數列長度爲 0)。

複雜度分析:

  • 時間複雜度:

循環最內層語句的執行次數爲:(n - 1)*(n - 1 - i)。保留最高指數,得到時間複雜度爲:O(n2)。

  • 空間複雜度:

該算法中,對數組的操作都是就地操作(in-place),沒有用到額外的存儲空間,因此空間複雜度爲:O(1)。

  • 穩定性

在對相鄰數據進行比較時,只有在 arr[i] > arr[j](i > j) 的情況下,才進行交互位置的操作,這樣就可以保證相等的兩個元素在完成比較後,依舊保留原本的前後相對位置。因此,冒泡排序是穩定的。

代碼實現:

// 冒泡排序(升序)
function bubbleSort(arr) {
	for (let i = 0; i < arr.length - 1; i++) {
		for (let j = 0; j < arr.length - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
			}
		}
	}
	return arr;
}

bubbleSort([3, 1, 6, 5, 7]);

2. 快速排序

基本思想:

  1. 選擇數列中的一個元素(在這選擇待比較數列第一個元素)作爲基準(pivot);
  2. 把小於基準值的元素放到基準的左邊,把大於基準值的元素放到基準的右邊,與基準值大小相等的元素放置在左邊還是右邊都可以,這一操作稱爲分區(partition)。至此,可以得到左邊和右邊的 2 個子數列;
  3. 對得到的子數列重複進行步驟 1、2,直至每個分區操作的數列都只剩下一個元素,這時候,整個數列就有序了。

複雜度分析:

  • 時間複雜度:

對應一個長度爲 n 的數組,需要進行 log2n 次分區操作;完成每次分區操作,需要對每個數據都進行比較操作,總的要進行 n 次比較。因此,快速排序的時間複雜度爲:O(nlog2n)。

  • 空間複雜度:

對數組元素的操作是就地操作,不會消耗額外的存儲空間。因此,快速排序對存儲空間的消耗就只有 log2n 次的分區操作了。所以,快速排序的空間複雜度爲:O(log2n)。

  • 穩定性:

對於數組 [2,5,5,3,1,7],使用快速排序來排序後,2 個 5 的前後相對位置變了。因此,快速排序是不穩定的。

代碼實現:

function exchange(arr, i, j) {
	[arr[i], arr[j]] = [arr[j], arr[i]];
}

function partition(arr, left, right) {
	let pivot = arr[left];
	let pivotIndex = left;
	if (left >= right) {
		return;
	}
	while (left < right) {
		while (arr[right] > pivot && left < right) {
			right--;
		}
		while (arr[left] <= pivot && left < right) {
			left++;
		}
		if (left < right) {
			exchange(arr, left, right);
		}
	}
	// left === right
	exchange(arr, pivotIndex, left);
	return left;
}

function quickSort(arr, left, right) {
	left = typeof left !== 'number' || Number.isNaN(left) ? 0 : left;
	right = typeof right !== 'number' || Number.isNaN(right) ? arr.length - 1 : right;
	if (left < right) {
		let pivotIndex = partition(arr, left, right);
		quickSort(arr, left, pivotIndex - 1);
		quickSort(arr, pivotIndex + 1, right);
	}
	return arr;
}

quickSort([3, 1, 2]);

3. 簡單插入排序

基本思想:

  1. 默認地,將數組中的第一個元素作爲有序數組, 剩下的元素作爲無序數組;
  2. 從後往前遍歷有序數組,找到合適的位置,插入無序數組的首個元素。至此,有序數組中增加一個元素,無序數組中減少一個元素;
  3. 重複進行步驟 2,直至無序數組中的全部元素都插入到了有序數組中。

複雜度分析:

  • 時間複雜度:

算法中出現了嵌套 2 層的循環。因此,簡單插入排序的時間複雜度爲:O(n2)。

  • 空間複雜度:

算法中對數組是就地操作的,沒有消耗額外的空間。因此,簡單插入排序的空間複雜度爲:O(1)。

  • 穩定性:

遍歷有序數組的過程中,是將比待插入數組元素大的元素向後移動,否則就將待插入元素插至比較元素之後(這樣的話,滿足:比較元素 <= 待插入元素),這樣的話,可以保證相同元素之間前後相對順序。因此,插入排序是穩定的。

代碼實現:

function insertionSort(arr) {
	// 遍歷無序數列
	for (let i = 1; i < arr.length; i++) {
		// 待插入的一個無序數據
		let current = arr[i];
		// 默認第一個數據爲有序的
		let j = i - 1;
		// 從後往前遍歷有序數列
		while (j >= 0 && current < arr[j]) {
			// 將有序數列中大於無序數據的部分從後往前依次向後移動
			arr[j + 1] = arr[j];
			j--;
		}
		arr[j + 1] = current;
	}
	return arr;
}

insertionSort([3, 1, 6, 5, 7]);

4. shell 排序(縮小增量排序)

基本思想:

  1. 選擇一個增量序列t1,t2,…,tk,其中 ti > tj,tk = 1;
  2. 按增量序列個數 k,對序列進行 k 趟排序;
  3. 每趟排序,根據對應的增量ti,將待排序列分割成若干長度爲m 的子序列,分別對各子表進行直接插入排序。僅增量因子爲1 時,整個序列作爲一個表來處理,表長度即爲整個序列的長度。

複雜度分析:

  • 時間複雜度:

與增量序列的選擇有關,太複雜了,本人是推不出,就記一個 O(n1.3) 好了。

  • 空間複雜度:

在這個算法中,對數組的操作都是就地完成的,不會消耗額外的存儲空間。因此,shell 排序的空間複雜度爲:O(1)。

  • 穩定性:

不穩定

代碼實現:

function shellSort(arr) {
	for (let gap = Math.floor(arr.length / 2); gap > 0; gap = Math.floor(gap / 2)) {
		for (let i = gap; i < arr.length; i++) {
			let current = arr[i];
			let j = i;
			while (j - gap >= 0 && arr[j - gap] > arr[j]) {
				arr[j - gap + 1] = arr[j - gap];
				j = j - gap;
			}
			arr[j] = current;
		}
	}
	return arr;
}

shellSort([3, 1, 6, 5, 7]);

5. 簡單選擇排序

基本思想:

  1. 初始時,認爲:數組中有序數列爲空,無序數列爲整個數組。
  2. 繼續遍歷無序數列,找出其中的最小值,並把它與有序數列的末尾後面的一個元素交換位置;
  3. 重複進行步驟 2,經過 n - 1 輪的循環後,整個數組就有序了。
    換成一句話概況: 選擇未排序的數列中的最小值,放在未排序數列的首位。

複雜度分析:

  • 時間複雜度:

2 層嵌套的循環。因此,選擇排序的時間複雜度爲:O(n2)。

  • 空間複雜度:

數組是就地操作的。因此,選擇排序的空間複雜度爲:O(1)。

  • 穩定性:

對於數組 [5, 8, 5, 2, 9],經過選擇排序進行排序之後,2 個 5 的前後相對順序就被破壞了。因此,選擇排序是不穩定的。

代碼實現:

function selectionSort(arr) {
	for (let i = 0; i < arr.length - 1; i++) {
		let minIndex = i;
		for (let j = i + 1; j < arr.length; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
	}
	return arr;
}

selectionSort([3, 1, 6, 5, 7, 6]);

6. 堆排序

基本思想:

  1. 將初始待排序關鍵字序列 (R1,R2….Rn) 構建成大頂堆,此堆爲初始的無序區;
  2. 將堆頂元素R[1]與最後一個元素R[n]交換,此時得到新的無序區 (R1,R2,……Rn-1) 和新的有序區 (Rn) ,且滿足 R[1,2…n-1]<=R[n] ;
  3. 由於交換後新的堆頂 R[1] 可能違反大頂堆的性質,因此需要對當前無序區 (R1,R2,……Rn-1) 調整爲新堆;
  4. 然後再次將 R[1] 與無序區最後一個元素交換,得到新的無序區 (R1,R2….Rn-2) 和新的有序區 (Rn-1,Rn) 。不斷重複此過程直到有序區的元素個數爲 n-1 ,則整個排序過程完成。

複雜度分析:

  • 時間複雜度:

O(nlog2n)

  • 空間複雜度:

數組是就地操作的。因此,選擇排序的空間複雜度爲:O(1)。

  • 穩定性:

不穩定

代碼實現:

function exchange(arr, i, j) {
	[arr[i], arr[j]] = [arr[j], arr[i]];
}

function shiftDown(arr, i, length) {
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		if (j + 1 < length && arr[j] < arr[j + 1]) {
			j++;
		}
		if (arr[j] > arr[i]) {
			exchange(arr, i, j);
			i = j;
		} else {
			break;
		}
	}
}

function heapSort(arr) {
	// 從下至上,從右至左將數據初始化爲大頂堆
	for (let i = Math.floor(arr.length / 2 - 1); i >= 0; i--) {
		// i 爲非葉子節點
		shiftDown(arr, i, arr.length);
	}

	// 將棧頂元素與未排序數列最後一個數據交換,交換後需要將剩下數據組成的數調整爲大頂堆
	for (let i = arr.length - 1; i > 0; i--) {
		exchange(arr, 0, i);
		shiftDown(arr, 0, i);
	}

	return arr;
}

heapSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2]);

7. 二路歸併排序

基本思想:

  1. 將一個長度爲 n 的數組分割爲 2 個長度爲 n / 2 的數組;
  2. 對 2 個數組分別遞歸地調用歸併排序;
  3. 將排好序的 2 個數組合併爲 1 個數組。

複雜度分析:

  • 時間複雜度:

數列的歸併樹高度爲 log2n,每層總的需要進行約 n 次比較。因此,歸併排序的時間複雜度爲 O(nlog2n)。

  • 空間複雜度:

代碼執行過程中,需要一個長度爲 n 的數組來存儲排序結果。因此,歸併排序的空間複雜度爲:O(n)。

  • 穩定性:

合併排序好的 2 個數組的時候,判斷條件 left[0] <= right[0] 爲 true 時,說明 left 數組和 right 數組的首元素相同,這時候 選擇先將 left 數組的首元素移入結果數組中, 就能保證這 2 個相同元素的前後相對位置與原來的一致。因此,歸併排序是穩定的。

代碼實現:

function merge(left, right) {
	let result = [];
	while (left.length > 0 && right.length > 0) {
		if (left[0] <= right[0]) {
			result.push(left.shift());
		} else {
			result.push(right.shift());
		}
	}
	return result.concat(left, right);
}

function mergeSort(arr) {
	if (arr.length <= 1) {
		return arr;
	}
	let middleIndex = Math.floor(arr.length / 2);
	return merge(mergeSort(arr.slice(0, middleIndex)), mergeSort(arr.slice(middleIndex)));
}

mergeSort([3, 1, 6, 5, 7]);

8. 計數排序

基本思想:

  1. 找到數組中的最大的元素,記爲 maxValue;
  2. 創建一個長度爲 maxValue + 1 的數據,用作後續的計數數組 countingArr;
  3. 遍歷數組 arr,累計 countArr[arr[i]] 的值;
  4. 遍歷 countingArr,反向填充得到結果數組 result。

當k不是很大並且序列比較集中時,計數排序是一個很有效的排序算法

複雜度分析:

將數組的最大值記爲 k

  • 時間複雜度:

n + (n + k) => O(n + k)

  • 空間複雜度:

算法需要創建一個長度爲 n + k 的計數數組 和一個長度爲 n 的結果數組。因此,空間複雜度爲 O(n + k)。

  • 穩定性:

該算法不會改變原數組的元素的位置。因此,計數排序是穩定的。

代碼實現:

function countingSort(arr, maxValue) {
	maxValue = maxValue || Math.max(...arr);
	let countingArr = new Array(maxValue + 1);
	let result = [];
	for (let i = 0; i < arr.length; i++) {
		if (!countingArr[arr[i]]) {
			// 初始化鍵值對應的計數值
			countingArr[arr[i]] = 0;
		}
		// 鍵值對應的數據出現,計數值加一
		countingArr[arr[i]]++;
	}
	for (let i = 0; i < countingArr.length; i++) {
		while (countingArr[i] > 0) {
			result.push(i);
			countingArr[i]--;
		}
	}
	return result;
}

countingSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2], 9);

9. 桶排序

基本思想:

  1. 將數據分配到桶中;
  2. 對桶中的數據進行排序;
  3. 將排序好的數據合併到結果數組中。

複雜度分析:

  • 時間複雜度:

O(n + k)

  • 空間複雜度:

O(n + k)

  • 穩定性:

穩定,理由同計數排序。

代碼實現:

function insertionSort(arr) {
	// 遍歷無序數列
	for (let i = 1; i < arr.length; i++) {
		// 待插入的一個無序數據
		let current = arr[i];
		// 默認第一個數據爲有序的
		let j = i - 1;
		// 從後往前遍歷有序數列
		while (j >= 0 && current < arr[j]) {
			// 將有序數列中大於無序數據的部分從後往前依次向後移動
			arr[j + 1] = arr[j];
			j--;
		}
		arr[j + 1] = current;
	}
	return arr;
}

function bucketSort(arr, size) {
	let bucketSize = size || 5;
	let result = [];
	if (arr.length <= 1) {
		return arr;
	}
	let minValue = arr[0];
	let maxValue = arr[0];
	// 找到數組中的最大值和最小值
	for (let i = 1; i < arr.length; i++) {
		if (minValue > arr[i]) {
			minValue = arr[i];
		} else if (maxValue < arr[i]) {
			maxValue = arr[i];
		}
	}

	let bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
	let buckets = new Array(bucketCount);
	// 初始化桶
	for (let i = 0; i < bucketCount; i++) {
		buckets[i] = [];
	}
	// 將數據分配到桶中
	for (let i = 0; i < arr.length; i++) {
		let bucketIndex = Math.floor((arr[i] - minValue) / bucketSize);
		buckets[bucketIndex].push(arr[i]);
	}
	// 使用插入排序,對桶內數據進行排序
	for (let i = 0; i < buckets.length; i++) {
		insertionSort(buckets[i]);
		// 將排序完成的數據放進 result 數組中
		result = [...result, ...buckets[i]];
	}
	return result;
}

bucketSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2]);

10. 基數排序

基本思想:

  1. 取得數組中的最大數,並取得位數;
  2. arr爲原始數組,從最低位開始取每個位組成 radix 數組;
  3. 對 radix 進行計數排序(利用計數排序適用於小範圍數的特點);

複雜度分析:

  • 時間複雜度:

O(n * k)

  • 空間複雜度:

O(n + k)

  • 穩定性:

穩定,理由同計數排序。

代碼實現:

let counter = [];
function radixSort(arr, maxDigit) {
	let mod = 10;
	let dev = 1;
	for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
		for (let j = 0; j < arr.length; j++) {
			let bucket = parseInt((arr[j] % mod) / dev);
			if (counter[bucket] == null) {
				counter[bucket] = [];
			}
			counter[bucket].push(arr[j]);
		}
		let pos = 0;
		for (let j = 0; j < counter.length; j++) {
			let value = null;
			if (counter[j] != null) {
				while ((value = counter[j].shift()) != null) {
					arr[pos++] = value;
				}
			}
		}
	}
	return arr;
}

radixSort([4, 6, 8, 5, 9, 1, 2, 5, 3, 2], 1);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章