JS 实现 10 大经典排序算法

0. 算法复杂度
排序算法时间复杂度(平均)空间复杂度稳定性
冒泡排序O(n2)O(1)稳定
快速排序O(nlogn)O(1)不稳定
简单插入排序O(n2)O(1)稳定
shell 排序O(n1.3)O(1)不稳定
简单选择排序O(n2)O(1)不稳定
堆排序O(nlogn)O(1)不稳定
二路归并排序O(nlogn)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)。

  • 空间复杂度:

对数组元素的操作是就地操作,不会消耗额外的存储空间。所以,快速排序的空间复杂度为:O(1)。

  • 稳定性:

对于数组 [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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值