目录
1. 冒泡排序 (Bubble Sort)
2. 选择排序 (Selection Sort)
3. 插入排序 (Insertion Sort)
4. 快速排序 (Quick Sort)
5. 归并排序 (Merge Sort)
6. 堆排序 (Heap Sort)
7.希尔排序(Shell Sort)
8.计数排序(Counting Sort)
排序算法 | 时间复杂度 | 空间复杂度 | 备注 | 类型 |
冒泡排序 | 最好情况: O(n) 平均情况: O(n^2) 最坏情况: O(n^2) | O(1) | 原地排序,只需常量级额外空间 稳定性:稳定 | 简单排序 |
选择排序 | 最好情况: O(n^2) 平均情况: O(n^2) | O(1) | 原地排序,只需常量级额外空间 稳定性:不稳定 | |
插入排序 | 最好情况: O(n) 平均情况: O(n^2) 最坏情况: O(n^2) | O(1) | 原地排序,只需常量级额外空间 稳定性:稳定 | |
快速排序 | 最好情况: O(n log n) 平均情况: O(n log n) 最坏情况: O(n^2 | O(log n) | 递归调用栈空间,最好情况O(log n),最坏情况O(n),但平均情况为O(log n) 稳定性:不稳定 | 分治排序 |
归并排序 | 最好情况:O(n log n) 平均情况: O(n log n) 最坏情况: O(n log n) | O(n) | 需要额外的临时数组来存储合并结果 稳定性:稳定 | |
堆排序 | 最好情况: O(n log n) 平均情况: O(n log n) 最坏情况: O(n log n) | O(1) | 原地排序,堆的调整过程在数组内部进行,只需常量级额外空间(不考虑递归实现,若考虑递归则与快速排序类似) 稳定性:不稳定 | 树状排序 |
希尔排序 | 最好情况: O(n log n) 平均情况: O(n1.75) 最坏情况: O(n ^2) | O(1) | 希尔排序是一种原地排序算法,它在原始数组上进行元素的交换和移动,不需要额外的辅助数组 稳定性:不稳定 | 其它 |
计数排序 | 最好情况: O(n+k) 平均情况: O(n+k) 最坏情况: O(n+k) | O(n+k) | 计数排序在实现过程中,主要涉及到数组的访问和修改,通常不需要显式地使用堆(用于动态内存分配)或栈(用于函数调用和局部变量存储)的额外复杂操作。 稳定性:稳定 |
1. 冒泡排序 (Bubble Sort)
时间复杂度:
- 最好情况: O(n)
- 平均情况: O(n^2)
- 最坏情况: O(n^2)
function bubbleSort(arr) { let n = arr.length; for (let i = 0; i < n - 1; i++) { for (let j = 0; j < n - 1 - i; j++) { if (arr[j] > arr[j + 1]) { // 交换元素 [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; } } } return arr;
}
2. 选择排序 (Selection Sort)
原理:
选择排序是一种简单直观的排序算法。它的工作原理是首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
具体步骤如下:
- 从未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(或最大)元素,放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
特点:
- 选择排序的时间复杂度为O(n^2),其中n是待排序元素的数量。
- 选择排序是一种原地排序算法,因为它只需要一个额外的空间来存储当前找到的最小(或最大)元素。
- 选择排序不是稳定的排序算法,因为相同元素的相对位置可能会在排序过程中发生改变。
时间复杂度:
- 最好情况: O(n^2)
- 平均情况: O(n^2)
- 最坏情况: O(n^2)
function selectionSort(arr) { let n = arr.length; for (let i = 0; i < n - 1; i++) { let minIndex = i; for (let j = i + 1; j < n; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } // 交换元素 [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]; } return arr;
}
3. 插入排序 (Insertion Sort)
原理:
插入排序的工作方式是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,找到相应位置并插入时,不需要移动元素,只需将要插入的元素移动到插入点即可。
具体步骤如下:
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,则将该元素移到下一位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置。
- 将新元素插入到该位置后。
- 重复步骤2~5,直到所有元素均排序完毕。
特点:
- 插入排序的时间复杂度为O(n^2),在数据规模较小时表现良好,特别是当数据基本有序时,时间复杂度可以接近O(n)。
- 插入排序是一种原地排序算法,因为它只需要一个额外的空间来存储当前正在插入的元素。
- 插入排序是稳定的排序算法,因为相同元素的相对位置在排序过程中不会发生改变。
时间复杂度:
- 最好情况: O(n)
- 平均情况: O(n^2)
- 最坏情况: O(n^2)
function insertionSort(arr) { let n = arr.length; for (let i = 1; i < n; i++) { let key = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j = j - 1; } arr[j + 1] = key; } return arr;
}
4. 快速排序 (Quick Sort)
原理:
快速排序是一种通过基准划分区块,再不断交换左右项的排序方式。它采用了分治法,减少了交换的次数。快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归或迭代进行,以此让整个数列变成有序序列。
具体步骤如下:
- 在待排序区间找到一个基准点(pivot),一般选择数组的第一个元素、最后一个元素或者随机选择一个元素。
- 逐个循环数组,将小于基准的项放在左侧,将大于基准的项放在右侧。一般通过交换的方式来实现。
- 对基准点左侧全部项和基点右侧全部项分别通过递归(或迭代)方式重复上述步骤,直到所有数组都交换完成。
时间复杂度:
- 最好情况: O(n log n)
- 平均情况: O(n log n)
- 最坏情况: O(n^2) (但可以通过随机化选择基准元素等方法优化)
function quickSort(arr) { if (arr.length <= 1) { return arr; } let pivot = arr[Math.floor(arr.length / 2)]; let left = []; let right = []; for (let i = 0; i < arr.length; i++) { if (i === Math.floor(arr.length / 2)) continue; if (arr[i] < pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), pivot, ...quickSort(right)];
}
5. 归并排序 (Merge Sort)
原理:
归并排序是一种分治算法,其工作原理是将未排序的列表划分为n个子列表,每个子列表包含一个元素(包含一个元素的列表被认为是有序的),然后重复合并子列表以生成新的有序子列表,直到只剩下一个子列表。
具体步骤如下:
- 分解:将待排序的n个元素的序列分成两个子序列,每个子序列包含n/2个元素。
- 解决:使用归并排序递归地排序两个子序列。
- 合并:将两个已排序的子序列合并成一个最终的排序序列。
时间复杂度:
- 最好情况: O(n log n)
- 平均情况: O(n log n)
- 最坏情况: O(n log n)
function mergeSort(arr) { if (arr.length <= 1) { return arr; } const middle = Math.floor(arr.length / 2); const left = arr.slice(0, middle); const right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right));
} function merge(left, right) { let result = []; let leftIndex = 0; let rightIndex = 0; while (leftIndex < left.length && rightIndex < right.length) { if (left[leftIndex] < right[rightIndex]) { result.push(left[leftIndex]); leftIndex++; } else { result.push(right[rightIndex]); rightIndex++; } } return result.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}
6. 堆排序 (Heap Sort)
原理:
堆排序(Heap Sort)是一种基于堆(Heap)这种数据结构的比较排序算法。堆是一个近似完全二叉树的结构,分为最大堆(Max Heap)和最小堆(Min Heap)。在最大堆中,每个节点的值都大于或等于其子节点的值;在最小堆中,每个节点的值都小于或等于其子节点的值。堆排序通常使用最大堆来实现升序排序
堆排序步骤:
- 构建最大堆:
- 将数组看作一个完全二叉树,构建最大堆。
- 从最后一个非叶子节点开始,向上依次调整堆,使得每个子树都满足最大堆的性质。
2.堆排序过程:
- 将堆顶元素(最大值)与堆的最后一个元素交换。
- 堆的大小减1,重新调整堆顶元素所在的子树,使其满足最大堆的性质。
- 重复上述步骤,直到堆的大小为1。
时间复杂度:
- 最好情况: O(n log n)
- 平均情况: O(n log n)
- 最坏情况: O(n log n)
function heapSort(arr) { let n = arr.length; // 构建最大堆 for (let i = Math.floor(n / 2) - 1; i >= 0; i--) { heapify(arr, n, i); } // 一个个从堆顶取出元素 for (let i = n - 1; i > 0; i--) { // 交换当前堆顶(最大值)和最后一个元素 [arr[0], arr[i]] = [arr[i], arr[0]]; // 重新调整堆 heapify(arr, i, 0); } return arr;
} function heapify(arr, n, i) { let largest = i; //最大子节点let left = 2 * i + 1; //左子节点let right = 2 * i + 2; //右子节点//如果左子节点存在且大于当前最大子节点if (left < n && arr[left] > arr[largest]) { largest = left; } //如果右子节点存在且大于当前最大子节点if (right < n && arr[right] > arr[largest]) { largest = right; } //如果最大值不是当前子节点,则交换 if (largest !== i) { [arr[i], arr[largest]] = [arr[largest], arr[i]]; heapify(arr, n, largest); }
}
7.希尔排序(Shell Sort)
希尔排序(Shell Sort)是一种基于插入排序的排序算法,通过比较距离较远的元素来工作,从而减少了数据移动的次数,提高了效率。希尔排序的关键在于间隔序列的选择,常见的间隔序列有希尔增量(Shell Increment)和希伯特增量(Hibbard Increment)等。
希尔排序算法步骤
- 选择间隔序列:选择一个初始间隔序列,通常从数组长度的一半开始,逐步缩小间隔直到为1。
- 分组排序:根据当前间隔,将数组分成多个子数组,每个子数组包含间隔为当前间隔的元素。
- 插入排序:对每个子数组进行插入排序。
- 缩小间隔:减小间隔,重复步骤2和3,直到间隔为1。
- 最终排序:当间隔为1时,整个数组进行一次插入排序,此时数组已经接近有序,插入排序效率较高。
时间复杂度:
- 最好情况: O(n log n)
- 平均情况: O(n1.75)
- 最坏情况: O(n ^2)
function shellSort(arr) { let n = arr.length; let gap = Math.floor(n / 2); // 不断缩小间隔,直到间隔为1 while (gap > 0) { // 对每个子数组进行插入排序 for (let i = gap; i < n; i++) { let temp = arr[i]; let j; // 对当前间隔的子数组进行插入排序 for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = temp; } // 缩小间隔 gap = Math.floor(gap / 2); } return arr;
} // 示例使用
let arr = [12, 34, 54, 2, 3];
console.log("排序前: ", arr);
let sortedArr = shellSort(arr);
console.log("排序后: ", sortedArr);
8.计数排序(Counting Sort)
计数排序(Counting Sort)是一种线性时间复杂度的排序算法,特别适用于一定范围内的整数排序。它的基本思想是通过计数来确定每个元素在排序后数组中的位置。
计数排序步骤:
- 找出待排序数组中的最大值和最小值:这是为了确定计数数组的范围。
- 创建计数数组:计数数组的长度设置为最大值与最小值之差加一,并将所有元素初始化为0。计数数组的每个索引对应原始数组中的可能值,而计数数组的值则记录该值在原始数组中出现的次数。
- 统计每个元素出现的次数:遍历原始数组,对每个元素进行计数,并将结果累加到计数数组对应索引的位置。
- 创建并填充排序后的数组:确保根据 count 数组中的计数,将正确的值(即原始值,经过偏移 min 后再偏移回来的值)写回到数组中。这样,计数数组中的每个值就表示了对应元素及其之前所有元素在排序后数组中的位置偏移量。
- 构建排序后的数组:根据计数数组的信息,将原始数组中的元素按照计算出的位置偏移量依次放入新的数组中,从而得到排序后的数组
时间复杂度:
- 最好情况: O(n+k)
- 平均情况: O(n+k)
- 最坏情况: O(n+k)
//计数排序function countSort(arr) {if (arr.length === 0) return arr; // 如果数组为空,直接返回 let max = Math.max(...arr);let min = Math.min(...arr);let count = new Array(max - min + 1).fill(0);// 统计每个元素出现的次数 for (let num of arr) {count[num - min]++;}// 创建并填充排序后的数组 let sortedArr = [];for (let i = 0; i < count.length; i++) {for (let j = 0; j < count[i]; j++) {sortedArr.push(i + min); // 将正确的值添加到新数组中 }}return sortedArr; // 返回排序后的新数组 }// 示例使用 let arrTest = [4, 2, 2, 8, 3, 3, 1];console.log(countSort(arrTest)); // 输出: [1, 2, 2, 3, 3, 4, 8]