图解十大排序算法
本文主要讲解算法里边最经典的十大排序算法。
- n: 数据规模
- k:“桶”的个数
- in-place(原地排序): 占用常数内存,不占用额外内存
- out-place: 占用额外内存
原地排序:指空间复杂度是的排序算法。
稳定性:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
1. 冒泡排序(Bubble Sort)
算法思想
冒泡排序是通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面,就像水底的气泡一样向上冒,故称这种排序方法为冒泡排序法。
算法步骤
- 先将数组中第 1 个元素与第 2 个元素进行比较,若前者大于后者,则两者交换位置,否则不交换;
- 然后将第 2 个元素与第 3 个元素比较,若前者大于后者,则两者交换位置,否则不交换;
- 依次重复,直到第 n - 1 个元素与第 n 个元素比较(或交换)为止。经过如此一趟排序,使得 n 个元素中值最大元素被安置在数组的第 n 个位置上;
- 接着,再对前 n - 1 个元素进行上述过程,使得前 n - 1 个元素中值最大元素被安置在第 n - 1 个位置上;
- 然后再对前 n - 2 个元素重复上述过程,直到某一趟排序过程中不出现元素交换位置的动作,排序结束。
图解思路
代码实现
- function bubbleSort(arr) {
- const { length } = arr;
- for (let i = 0; i < length; i++) {
- // 减去i是为了不再遍历已经排序好的元素
- for (let j = 0; j < length - 1 - i; j++) {
- if (arr[j] > arr[j + 1]) {
- [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
- }
- }
- }
- return arr;
- }
复杂度分析
时间复杂度:。冒泡排序的每一轮都要遍历所有元素,总共遍历(元素数量 - 1)轮。
空间复杂度:。只需要使用常数的额外空间。
2. 选择排序
算法思想
找到数据结构中的最小值并将其放置在第一位,接着找到第二小的值并将其放在第二位,以此类推。
算法步骤
- 每一趟排序开始,先将
min = i
(即暂时假设序列的第 i 个元素为最小值,经过比较后,再确定最小值元素的位置)。 - 第 i 趟比较结束时,这
n − i + 1
个元素中值最小的元素为下标min
对应的元素。此时,如果min == i
,说明值最小元素就是这n − i + 1
个元素的第 1 个元素,意味着此趟排序不必进行元素交换;否则交换第min
个和第i
个元素。 - 以此重复,直到最后一个元素
图解思路
代码实现
- function selectionSort(arr) {
- const { length } = arr;
- let min = 0;
- for (let i = 0; i < length - 1; i++) {
- min = i;
- for (let j = i; j < length; j++) {
- if (arr[min] > arr[j]) {
- min = j;
- }
- }
- if (min !== i) {
- [arr[min], arr[i]] = [arr[i], arr[min]];
- }
- }
- return arr;
- }
复杂度分析
时间复杂度:。对于具有 n 个元素的序列采用选择排序方法要经过 n - 1 趟排序,同时每一趟要比较的元素为 n - i + 1 个。
空间复杂度:。只需要使用常数的额外空间。
3. 插入排序
算法思想
将整个序列切分为两部分:前 i - 1 个元素是有序序列,后 n - i + 1 个元素是无序序列。每一次排序,将无序序列的首元素,在有序序列中找到相应的位置并插入。
算法步骤
- 将第一个元素作为一个有序序列,将第 2 ~ n - 1 个元素作为无序序列;
- 从头至尾一次扫描无序序列,将扫描到的每个元素插入到有序序列的适当位置上。
图解思路
代码实现
- function insertionSort(arr) {
- const { length } = arr;
- for (let i = 1; i < length; i++) {
- let j = i;
- let temp = arr[i];
- while (j >= 1 && arr[j - 1] > temp) {
- arr[j] = arr[j - 1];
- j--;
- }
- arr[j] = temp;
- }
- return arr;
- }
复杂度分析
时间复杂度: 。需遍历一次数组,同时遍历前面已排序的区间比较。
空间复杂度:。只需要使用常数的额外空间。
4. 希尔排序
算法思想
将整个序列按照一定的间隔取值划分为若干个子序列,每个子序列分别进行插入排序,然后逐渐缩小间隔进行下一轮划分子序列和插入排序。直至最后一轮排序间隔为 1,对整个序列进行插入排序。
算法步骤
- 首先确定一个元素间隔数 gap,然后将参加排序的序列按此间隔数从第 1 个元素开始一次分成若干个子序列,即分别将所有位置相隔为 gap 的元素视为一个子序列,在各个子序列中采用某种排序方法进行排序(这里使用插入排序);
- 然后减少间隔数,并重新将整个序列按新的间隔数分成若干个子序列,再分别对各个子序列进行排序;如此下去,直到间隔数 gap = 1。
图解思路
代码实现
- function shellSort(arr) {
- const length = arr.length;
- let gap = Math.floor(length / 2);
- while (gap > 0) {
- // 从第gap个元素开始,逐个对其所在组进行直接插入排序操作
- for (let i = gap; i < length; i++) {
- let temp = arr[i];
- let j = i;
- while (j >= gap && arr[j - gap] > temp) {
- arr[j] = arr[j - gap];
- j -= gap;
- }
- arr[j] = temp;
- }
- gap = Math.floor(gap / 2);
- }
- return arr;
- }
复杂度分析
时间复杂度:。
空间复杂度:。只需要使用常数的额外空间。
5. 归并排序
算法思想
归并排序是一种分而治之算法。其思想是将原数组切分成较小的数组,直到每个小数组只有一个位置,接着将小数组归并成较大的数组,直到最后只有一个排序完毕的大数组。
算法步骤
将数组从中间分成前后两部分,然后对前后两部分分别排序,再将排序好的两部分合并在一起。
图解思路
代码实现
- function merge(left, right) {
- let i = 0;
- let j = 0;
- const result = [];
- while (i < left.length && j < right.length) {
- result.push(left[i] < right[j] ? left[i++] : right[j++]);
- }
- return result.concat(i < left.length ? left.slice(i) : right.slice(j));
- }
- function mergeSort(arr) {
- if (arr.length > 1) {
- const middle = Math.floor(arr.length / 2);
- const left = mergeSort(arr.slice(0, middle));
- const right = mergeSort(arr.slice(middle));
- arr = merge(left, right);
- }
- return arr;
- }
复杂度分析
时间复杂度:。
空间复杂度:。
6. 快速排序
算法思想
快速排序:在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。
算法步骤
选择一个基准元素(下面选择数组的中间元素),将比它大的元素移动到右边,比它小的元素移动到左边,然后返回索引位置 i
(分开左小右大的索引位置);重复前面的步骤,直到索引位置不可再分为止(也可以说只有一个元素为止)。
图解思路
代码实现
- function quickSort(arr) {
- return quick(arr, 0, arr.length - 1);
- }
- function quick(arr, left, right) {
- let index;
- if (array.length > 1) {
- index = partition(arr, left, right);
- if (left < index - 1) {
- quick(arr, left, index - 1);
- }
- if (index < right) {
- quick(arr, index, right);
- }
- }
- }
- function partition(arr, left, right) {
- const pivot = arr[Math.floor((right + left) / 2)];
- let i = left;
- let j = right;
- while (i <= j) {
- while (arr[i] < pivot) {
- i++;
- }
- while (arr[j] > pivot) {
- j--;
- }
- if (i <= j) {
- [arr[i], arr[j]] = [arr[j], arr[i]];
- i++;
- j--;
- }
- }
- return i;
- }
复杂度分析
时间复杂度:。
空间复杂度:。只需要使用常数的额外空间。
7. 堆排序
算法思想
借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆维持大顶堆性质。
算法步骤
- 把无序数组构建成二叉堆,需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆;
- 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
图解思路
代码实现
- function heapSort(arr) {
- buildHeap(arr, arr.length);
- let k = arr.length - 1;
- while (k > 0) {
- // 将堆顶元素放到当前堆的最后一位
- [arr[k], arr[0]] = [arr[0], arr[k]];
- k--;
- // 将未排序的元素重新构建大顶堆
- heapify(arr, k, 0);
- }
- return arr;
- }
- // n表示堆中元素个数
- function buildHeap(arr, n) {
- //从第一个非叶子结点从下至上
- for (let i = Math.floor((n - 1) / 2); i >= 0; i--) {
- heapify(arr, n, i);
- }
- }
- // 堆化
- function heapify(arr, n, i) {
- while (true) {
- let maxPos = i;
- // 跟左子节点比较,取最大值的位置
- if (i * 2 + 1 <= n && arr[i] < arr[i * 2 + 1]) {
- maxPos = i * 2 + 1;
- }
- // 跟右子节点比较,取最大值的位置
- if (i * 2 + 2 <= n && arr[i * 2 + 2] > arr[maxPos]) {
- maxPos = i * 2 + 2;
- }
- if (maxPos === i) {
- break;
- }
- [arr[i], arr[maxPos]] = [arr[maxPos], arr[i]];
- i = maxPos;
- }
- }
复杂度分析
时间复杂度:。
空间复杂度:。只需要使用常数的额外空间。
8. 计数排序
算法思想
计数排序和其他排序不一样,并不是通过比较元素的大小来实现的,而是通过统计所有相同元素出现的次数来进行排序,思路与其他的排序大有区别。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法步骤
- 找出待排序数组中最大值元素和最小值元素;
- 统计数组中每个值为 i 的元素出现的次数,存入数组的第 i 项;
- 对所有的计数累加(从 counts 中的第一个元素开始,每一项和前一项累加)
- 反向填充目标数组:将每个元素 i 放在新数组的第 counts[i] 项,每放一个元素就要将 counts[i] -= 1。
图解思路
代码实现
- function countSort(arr) {
- let maxValue = arr[0];
- // 找到最大值
- for (let i = 1; i < arr.length; i++) {
- if (arr[i] > maxValue) {
- maxValue = arr[i];
- }
- }
- const temp = new Array(maxValue + 1);
- temp.fill(0);
- // 计算每个元素的个数,放入temp中
- for (let i = 0; i < arr.length; i++) {
- temp[arr[i]]++;
- }
- // // 将结果拷贝给arr数组
- let p = 0;
- for (let i = 0; i < temp.length; i++) {
- let j = temp[i];
- while (j > 0) {
- arr[p++] = i;
- j--;
- }
- }
- return arr;
- }
复杂度分析
时间复杂度:。
空间复杂度:。其中n为数组中最大元素的值,需要长度为n的临时数组来计数。
9. 桶排序
算法思想
桶排序(Bucked Sort)是将待排序集合中处于同一值域的元素存入同一桶中,也就是根据元素值特性将集合拆分为多个区域,则拆分后形成的多个桶,从值域上开是处于有序状态的。对每个桶中元素进行排序,则所有桶中元素构成的集合是已排序的。
算法步骤
- 将区间划分为
n
个相同大小的子区间,每个区间称为一个桶; - 遍历数组,将每个元素装入对应的桶中;
- 对每个桶内的元素单独排序(使用插入、归并、快排等算法);
- 最后按照顺序将桶内的元素合并起来。
图解思路
代码实现
- function bucketSort(arr) {
- if (arr.length < 2) {
- return arr;
- }
- // 创建桶
- const buckets = createBuckets(arr);
- return sortBuckets(buckets);
- }
- function createBuckets(arr) {
- // 每个桶中元素个数,为了方便,这里用3
- const bucketSize = 3;
- let minValue = arr[0];
- let maxValue = arr[0];
- for (let i = 1; i < arr.length; i++) {
- if (arr[i] < minValue) {
- minValue = arr[i];
- } else if (arr[i] > maxValue) {
- maxValue = arr[i];
- }
- }
- // 桶的个数
- const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
- const buckets = [];
- for (let i = 0; i < bucketCount; i++) {
- buckets[i] = [];
- }
- for (let i = 0; i < arr.length; i++) {
- const bucketIndex = Math.floor((arr[i] - minValue) / bucketSize);
- buckets[bucketIndex].push(arr[i]);
- }
- return buckets;
- }
- function sortBuckets(buckets) {
- const sortedArray = [];
- for (let i = 0; i < buckets.length; i++) {
- if (buckets[i].length) {
- // 用插入排序对每个桶排序
- insertionSort(buckets[i]);
- // 将排序后的数组平铺到sortedArray中
- sortedArray.push(...buckets[i]);
- }
- }
- return sortedArray;
- }
复杂度分析
时间复杂度:。其中n为数组中元素个数,k为桶个数。
空间复杂度:。其中n为数组中元素个数,k为桶个数。
10. 基数排序
算法思想
基数排序在计数排序的基础上进行了改进,将排序工作拆分为多个阶段,每一个阶段只根据一个字符进行排序,一共排序n轮(n为数据长度)。
算法步骤
基数排序会先基于最后一位有效位对数字开始排序,在下次迭代时,会基于第二个有效位进行排序,然后是第三个有效位,以此类推,直到没有待排序的有效位
图解思路
代码实现
- /**
- * maxDigit {number} 最大位数
- **/
- function radixSort(arr, maxDigit = 10) {
- if (arr.length < 2) {
- return arr;
- }
- let mod = 10;
- // 第几位开始排序(从后往前)
- let dev = 1;
- let counter = [];
- for (let i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
- // 根据有效位分配到桶(有效位相同的在一个桶)
- for (let j = 0; j < arr.length; j++) {
- // 获取当前有效位
- const bucket = Math.floor((arr[j] % mod) / dev);
- if (!counter[bucket]) {
- counter[bucket] = [];
- }
- counter[bucket].push(arr[j]);
- }
- // 如果只是一个桶中有数据,则说明排序完成
- if (counter[0].length === arr.length) {
- break;
- }
- let pos = 0;
- // 将桶中元素重新赋值给原数组(已根据有效位排序过)
- for (let j = 0; j < counter.length; j++) {
- let value;
- if (counter[j] && counter[j].length) {
- value = counter[j].shift();
- while (value >= 0) {
- arr[pos++] = value;
- value = counter[j].shift();
- }
- }
- }
- }
- return arr;
- }
复杂度分析
时间复杂度:。其中n为数组中元素个数,k为桶个数。
空间复杂度:。其中n为数组中元素个数,k为桶个数。
能力有限,水平一般,如有错误的,欢迎指正。