排序算法全解析 🚀
本文详细介绍常见的排序算法,包括原理解析、代码实现、性能分析等,帮助前端开发者更好地理解和应用这些算法。
目录 📑
冒泡排序 🫧
1. 算法思想
想象一下冒泡的过程:
- 就像气泡在水中上升一样,较大的数字会不断"上浮"到数组的右侧
- 通过相邻元素的比较和交换,每一轮都会将当前最大的数字冒泡到正确的位置
- 经过 n-1 轮冒泡后,数组就完成了排序
2. 代码实现与详解
js
/**
* 冒泡排序
* 时间复杂度: O(n²)
* 空间复杂度: O(1)
* 稳定性: 稳定
*/
for (let i = 0; i < arr.length; i++) {
let flag = true // 优化点:标记本轮是否发生交换
for (let j = 0; j < arr.length - i - 1; j++) {
// 相邻元素比较,大的上浮
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]] // ES6解构赋值交换
flag = false // 发生了交换,标记为false
}
}
// 如果一轮下来没有交换,说明已经有序
if(flag) break
}
3. 图解过程
初始数组: [5, 3, 8, 4, 2]
第一轮:
[3, 5, 8, 4, 2] ← 5和3交换
[3, 5, 8, 4, 2] ← 5和8不交换
[3, 5, 4, 8, 2] ← 8和4交换
[3, 5, 4, 2, 8] ← 8和2交换
结果:最大值8已到达正确位置
第二轮:
[3, 5, 4, 2, 8]
[3, 4, 5, 2, 8] ← 5和4交换
[3, 4, 2, 5, 8] ← 5和2交换
...
4. 性能分析
指标 | 结果 | 说明 |
---|---|---|
时间复杂度 | O(n²) | 两层嵌套循环 |
空间复杂度 | O(1) | 只需要一个临时变量 |
稳定性 | 稳定 | 相等元素不会交换位置 |
5. 优化思路
- 添加 flag 标志位,当某轮没有交换时提前退出
- 记录最后一次交换的位置,减少比较范围
- 同时从两端进行冒泡(鸡尾酒排序)
选择排序 🎯
1. 算法思想
想象你在整理扑克牌:
- 从未排序区域找到最小的牌
- 将它放到已排序区域的末尾
- 重复这个过程直到所有牌都排好序
2. 代码实现与详解
js
/**
* 选择排序
* 时间复杂度: O(n²)
* 空间复杂度: O(1)
* 稳定性: 不稳定
*/
let index
for (let i = 0; i < arr.length; i++) {
index = i // 记录最小值的索引
// 在未排序区域寻找最小值
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[index]) {
index = j
}
}
// 如果找到比当前位置更小的元素,交换它们
if (index != i) {
// 使用异或运算进行交换,无需额外空间
arr[i] = arr[i] ^ arr[index]
arr[index] = arr[i] ^ arr[index]
arr[i] = arr[i] ^ arr[index]
}
}
插入排序 🎯
1. 算法思想
想象你在打扑克牌:
- 每次抓到一张新牌时,都要将它插入到手中已经排好序的牌中
- 从右向左比较,找到合适的位置插入
- 就像整理手中的扑克牌一样,逐步构建有序序列
💡 插入排序对于小规模数据或基本有序的数据特别高效
2. 代码实现与详解
js
/**
* 插入排序
* @param {number[]} arr - 待排序数组
* @returns {number[]} - 排序后的数组
* 时间复杂度: O(n²)
* 空间复杂度: O(1)
* 稳定性: 稳定
*/
function insertionSort(arr) {
const len = arr.length;
// 从第二个元素开始,因为第一个元素可以认为已经被排序
for (let i = 1; i < len; i++) {
// 保存当前要插入的元素
const current = arr[i];
let j = i - 1;
// 寻找插入位置
while (j >= 0 && arr[j] > current) {
// 将较大的元素都向右移动一位
arr[j + 1] = arr[j];
j--;
}
// 插入到正确位置
arr[j + 1] = current;
}
return arr;
}
3. 图解过程
初始数组: [5, 3, 8, 4, 2]
第一轮: 3要插入到已排序区域[5]中
[5, 3, 8, 4, 2] → [3, 5, 8, 4, 2]
第二轮: 8要插入到已排序区域[3, 5]中
[3, 5, 8, 4, 2] → [3, 5, 8, 4, 2] (无需移动)
第三轮: 4要插入到已排序区域[3, 5, 8]中
[3, 5, 8, 4, 2] → [3, 4, 5, 8, 2]
第四轮: 2要插入到已排序区域[3, 4, 5, 8]中
[3, 4, 5, 8, 2] → [2, 3, 4, 5, 8]
4. 前端实际应用
js
/**
* 实际应用:对用户列表按注册时间排序
*/
class UserList {
constructor() {
this.users = [];
}
addUser(user) {
// 使用插入排序保持用户列表按注册时间有序
const newUser = {
...user,
registerTime: new Date(user.registerTime)
};
let i = this.users.length - 1;
while (i >= 0 &&
this.users[i].registerTime > newUser.registerTime) {
this.users[i + 1] = this.users[i];
i--;
}
this.users[i + 1] = newUser;
}
}
// 使用示例
const userList = new UserList();
userList.addUser({
name: 'Tom',
registerTime: '2023-01-01'
});
userList.addUser({
name: 'Jerry',
registerTime: '2023-02-01'
});
5. 性能分析
场景 | 时间复杂度 | 说明 |
---|---|---|
最好情况 | O(n) | 数组已经有序 |
最坏情况 | O(n²) | 数组完全逆序 |
平均情况 | O(n²) | 随机顺序 |
6. 优化思路
- 二分查找优化:使用二分查找快速找到插入位置
- 希尔排序:通过增量分组进行优化
- 缓存机制:对频繁插入的场景使用缓存
希尔排序 🐚
1. 算法思想
希尔排序是插入排序的改进版:
- 将数组按照一定间隔分组
- 对每组使用插入排序
- 逐步减小间隔,最后一轮间隔为1
- 类似于"大步快跑,小步调整"
2. 代码实现
js
/**
* 希尔排序
* @param {number[]} arr - 待排序数组
* @returns {number[]} - 排序后的数组
* 时间复杂度: O(n^1.3)
* 空间复杂度: O(1)
* 稳定性: 不稳定
*/
function shellSort(arr) {
const len = arr.length;
// 初始间隔设为长度的一半
let gap = Math.floor(len / 2);
while (gap > 0) {
// 对每个分组进行插入排序
for (let i = gap; i < len; i++) {
const 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;
}
归并排序 🔄
1. 算法思想
想象你在整理一摞扑克牌:
- 将牌分成两堆
- 分别整理好每一堆
- 最后将两堆有序的牌合并成一堆
- 这就是"分而治之"的思想
💡 归并排序的核心是:先分解,后合并。就像公司处理大项目时,先拆分成小组任务,各组完成后再整合。
2. 代码实现与详解
js
/**
* 归并排序
* @param {number[]} arr - 待排序数组
* @returns {number[]} - 排序后的数组
* 时间复杂度: O(nlogn)
* 空间复杂度: O(n)
* 稳定性: 稳定
*/
function mergeSort(arr) {
// 基础情况:数组长度为1时已经有序
if (arr.length <= 1) return arr;
// 分解:将数组分成两半
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// 递归排序左右两半
return merge(
mergeSort(left),
mergeSort(right)
);
}
/**
* 合并两个有序数组
* @param {number[]} left - 左半部分
* @param {number[]} right - 右半部分
* @returns {number[]} - 合并后的有序数组
*/
function merge(left, right) {
const 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));
}
3. 图解过程
原始数组: [5, 3, 8, 4, 2, 7, 1, 6]
分解过程:
[5, 3, 8, 4 | 2, 7, 1, 6]
[5, 3 | 8, 4] [2, 7 | 1, 6]
[5 | 3] [8 | 4] [2 | 7] [1 | 6]
合并过程:
[3, 5] [4, 8] [2, 7] [1, 6]
[3, 4, 5, 8] [1, 2, 6, 7]
[1, 2, 3, 4, 5, 6, 7, 8]
4. 前端实战应用
js
/**
* 实际应用:合并多个有序请求结果
*/
class RequestMerger {
/**
* 合并多个分页请求的结果
* @param {Promise[]} requests - 请求数组
* @returns {Promise<Array>} - 合并后的有序结果
*/
static async mergeRequests(requests) {
try {
// 并行执行所有请求
const results = await Promise.all(requests);
// 合并所有有序结果
return results.reduce((merged, current) => {
return this.merge(merged, current);
}, []);
} catch (error) {
console.error('合并请求失败:', error);
throw error;
}
}
/**
* 合并两个有序数组
*/
static merge(arr1, arr2) {
const result = [];
let i = 0, j = 0;
while (i < arr1.length && j < arr2.length) {
if (arr1[i].timestamp <= arr2[j].timestamp) {
result.push(arr1[i++]);
} else {
result.push(arr2[j++]);
}
}
return result
.concat(arr1.slice(i))
.concat(arr2.slice(j));
}
}
// 使用示例
async function getOrderedData() {
const requests = [
fetch('/api/data?page=1'),
fetch('/api/data?page=2'),
fetch('/api/data?page=3')
];
const mergedData = await RequestMerger.mergeRequests(requests);
return mergedData;
}
5. 性能分析
场景 | 时间复杂度 | 空间复杂度 | 说明 |
---|---|---|---|
最好情况 | O(nlogn) | O(n) | 任何情况下都是 |
最坏情况 | O(nlogn) | O(n) | 任何情况下都是 |
平均情况 | O(nlogn) | O(n) | 任何情况下都是 |
6. 优化思路
- 小规模数组使用插入排序
- 原地归并减少空间使用
- 并行化处理大数据集
- 缓存临时数组避免频繁分配
快速排序 ⚡
1. 算法思想
想象你在整理书架:
- 先选一本基准书(pivot)
- 把比基准书薄的放左边,厚的放右边
- 对左右两边的书重复这个过程
- 最终书架就按厚度排好序了
💡 快速排序是实际应用中最常用的排序算法,V8引擎的 Array.prototype.sort() 就使用了快排的思想。
2. 代码实现与详解
js
/**
* 快速排序
* @param {number[]} arr - 待排序数组
* @returns {number[]} - 排序后的数组
* 时间复杂度: 平均 O(nlogn),最坏 O(n²)
* 空间复杂度: O(logn)
* 稳定性: 不稳定
*/
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
// 获取分区点
const pivotIndex = partition(arr, left, right);
// 递归排序左右两部分
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
return arr;
}
/**
* 分区函数
* @param {number[]} arr - 待分区数组
* @param {number} left - 左边界
* @param {number} right - 右边界
* @returns {number} - 基准元素的最终位置
*/
function partition(arr, left, right) {
// 选择最右边的元素作为基准
const pivot = arr[right];
// i 是小于基准区域的边界
let i = left - 1;
// 将小于基准的元素移到左边
for (let j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
// 将基准元素放到正确的位置
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
return i + 1;
}
3. 优化版本
js
/**
* 优化的快速排序
* 1. 三数取中选择基准
* 2. 小数组使用插入排序
* 3. 处理重复元素
*/
function optimizedQuickSort(arr, left = 0, right = arr.length - 1) {
// 小数组使用插入排序
if (right - left <= 10) {
return insertionSort(arr, left, right);
}
// 三数取中选择基准
const mid = Math.floor((left + right) / 2);
if (arr[left] > arr[mid]) {
[arr[left], arr[mid]] = [arr[mid], arr[left]];
}
if (arr[left] > arr[right]) {
[arr[left], arr[right]] = [arr[right], arr[left]];
}
if (arr[mid] > arr[right]) {
[arr[mid], arr[right]] = [arr[right], arr[mid]];
}
const pivot = arr[mid];
// 三路快排处理重复元素
let lt = left; // less than pivot
let gt = right; // greater than pivot
let i = left + 1; // current element
while (i <= gt) {
if (arr[i] < pivot) {
[arr[lt], arr[i]] = [arr[i], arr[lt]];
lt++;
i++;
} else if (arr[i] > pivot) {
[arr[gt], arr[i]] = [arr[i], arr[gt]];
gt--;
} else {
i++;
}
}
// 递归排序左右两部分
optimizedQuickSort(arr, left, lt - 1);
optimizedQuickSort(arr, gt + 1, right);
return arr;
}
4. 前端实战应用
js
/**
* 实际应用:文件大小排序
*/
class FileSystem {
constructor() {
this.files = [];
}
/**
* 添加文件
* @param {Object} file - 文件对象
*/
addFile(file) {
this.files.push({
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
});
}
/**
* 获取按大��排序的文件列表
* @returns {Array} - 排序后的文件列表
*/
getSortedBySize() {
return quickSort([...this.files], 0, this.files.length - 1);
}
/**
* 自定义比较函数的快速排序
*/
customSort(compareFunc) {
const sort = (arr, left, right) => {
if (left >= right) return;
const pivot = arr[right];
let i = left - 1;
for (let j = left; j < right; j++) {
if (compareFunc(arr[j], pivot) <= 0) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
sort(arr, left, i);
sort(arr, i + 2, right);
};
const newFiles = [...this.files];
sort(newFiles, 0, newFiles.length - 1);
return newFiles;
}
}
// 使用示例
const fs = new FileSystem();
fs.addFile({name: 'doc.pdf', size: 1024});
fs.addFile({name: 'img.jpg', size: 2048});
// 按大小排序
const sizeSort = fs.getSortedBySize();
// 自定义排序(按名称)
const nameSort = fs.customSort((a, b) =>
a.name.localeCompare(b.name)
);
5. 性能分析
场景 | 时间复杂度 | 原因 |
---|---|---|
最好情况 | O(nlogn) | 每次都平均分割 |
最坏情况 | O(n²) | 已经有序或逆序 |
平均情况 | O(nlogn) | 随机数据 |
6. 优化思路
- 三数取中选择基准
- 小数组使用插入排序
- 三路快排处理重复元素
- 随机选择基准元素
- 尾递归优化
7. 注意事项 ⚠️
- 基准元素的选择会影响性能
- 处理重复元素时需要特别注意
- 递归深度可能导致栈溢出
- 不稳定性可能影响某些应用场景
堆排序 🌳
1. 算法思想
想象一个公司的组织架构:
- CEO在顶层,管理整个团队
- 每个经理管理自己的小组
- 调整时,高管可能下调,基层员工可能升职
- 这就像堆的调整过程
💡 堆排序利用了完全二叉树的特性,通过构建最大堆,不断将最大元素放到数组末尾来实现排序。
2. 基本概念
- 完全二叉树:除了最后一层,其他层都是满的,最后一层的节点都靠左排列
- 最大堆:父节点总是大于或等于子节点
- 节点关系:
- 父节点索引:
Math.floor((i-1)/2)
- 左子节点索引:
2*i + 1
- 右子节点索引:
2*i + 2
- 父节点索引:
3. 代码实现与详解
js
/**
* 堆排序
* @param {number[]} arr - 待排序数组
* @returns {number[]} - 排序后的数组
* 时间复杂度: O(nlogn)
* 空间复杂度: O(1)
* 稳定性: 不稳定
*/
function heapSort(arr) {
const len = arr.length;
// 构建最大堆
// 从最后一个非叶子节点开始向上调整
for (let i = Math.floor(len/2) - 1; i >= 0; i--) {
heapify(arr, len, i);
}
// 排序过程
for (let i = len - 1; i > 0; i--) {
// 将堆顶(最大值)交换到数组末尾
[arr[0], arr[i]] = [arr[i], arr[0]];
// 重新调整堆
heapify(arr, i, 0);
}
return arr;
}
/**
* 调整堆
* @param {number[]} arr - 待调整的数组
* @param {number} n - 堆的大小
* @param {number} i - 当前节点索引
*/
function heapify(arr, n, i) {
let largest = i; // 假设当前节点最大
const left = 2 * i + 1; // 左子节点
const 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);
}
}
4. 图解过程
原始数组: [4, 10, 3, 5, 1]
构建最大堆:
4 10 10
/ \ / \ / \
10 3 => 4 3 => 5 3
/ \ / \ / \
5 1 5 1 4 1
排序过程:
10 1 1
/ \ / \ / \
5 3 => 5 3 => 4 3
/ \ / \ /
4 1 4 10 5 10
5. 前端实战应用
js
/**
* 实际应用:任务优先级队列
*/
class PriorityQueue {
constructor() {
this.queue = [];
}
/**
* 添加任务
* @param {Object} task - 任务对象
* @param {number} priority - 优先级(1-5)
*/
enqueue(task, priority) {
this.queue.push({ task, priority });
this._bubbleUp(this.queue.length - 1);
}
/**
* 获取最高优先级任务
*/
dequeue() {
if (this.queue.length === 0) return null;
const top = this.queue[0];
const last = this.queue.pop();
if (this.queue.length > 0) {
this.queue[0] = last;
this._sinkDown(0);
}
return top.task;
}
/**
* 向上调整
*/
_bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.queue[parentIndex].priority >=
this.queue[index].priority) {
break;
}
[this.queue[parentIndex], this.queue[index]] =
[this.queue[index], this.queue[parentIndex]];
index = parentIndex;
}
}
/**
* 向下调整
*/
_sinkDown(index) {
const length = this.queue.length;
while (true) {
let highestPriority = index;
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
if (leftChild < length &&
this.queue[leftChild].priority >
this.queue[highestPriority].priority) {
highestPriority = leftChild;
}
if (rightChild < length &&
this.queue[rightChild].priority >
this.queue[highestPriority].priority) {
highestPriority = rightChild;
}
if (highestPriority === index) break;
[this.queue[index], this.queue[highestPriority]] =
[this.queue[highestPriority], this.queue[index]];
index = highestPriority;
}
}
}
// 使用示例
const taskQueue = new PriorityQueue();
taskQueue.enqueue({
id: 1,
name: '紧急bug修复'
}, 5);
taskQueue.enqueue({
id: 2,
name: '新功能开发'
}, 3);
taskQueue.enqueue({
id: 3,
name: '代码优化'
}, 1);
console.log(taskQueue.dequeue()); // 输出:紧急bug修复
6. 性能分析
操作 | 时间复杂度 | 说明 |
---|---|---|
建堆 | O(n) | 从最后一个非叶子节点开始调整 |
调整 | O(logn) | 树的高度决定 |
整体排序 | O(nlogn) | n次调整操作 |
7. 优化思路
- 数组预分配空间
- 迭代代替递归
- 批量建堆优化
- 缓存计算结果
8. 注意事项 ⚠️
- 堆是完全二叉树,不要破坏这个性质
- 注意索引计算的边界条件
- 父子节点的关系要始终保持
- 调整过程要考虑连锁反应
9. 练习题推荐 📝
- LeetCode 215: 数组中的第K个最大元素
- LeetCode 347: 前K个高频元素
- LeetCode 23: 合并K个排序链表
- LeetCode 295: 数据流的中位数
总结与对比 📊
1. 排序算法对比表
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 小数据量、近乎有序 |
选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | 小数据量、对稳定性无要求 |
插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 小数据量、近乎有序 |
希尔排序 | O(n^1.3) | O(n²) | O(1) | 不稳定 | 中等数据量 |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 | 大数据量、要求稳定 |
快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 | 大数据量、通用场景 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 | 大数据量、求TopK |
2. 如何选择排序算法?🤔
数据规模考虑
- 小规模数据(n < 50):插入排序
- 中等规模数据:快速排序
- 大规模数据:归并排序(稳定)或快速排序(高效)
数据特征考虑
- 近乎有序:插入排序
- 完全随机:快速排序
- 重复元素多:三路快排
- 对象排序:归并排序(稳定性好)
环境限制考虑
- 内存受限:堆排序
- CPU密集:快速排序
- 要求稳定:归并排序
- 并行处理:归并排序
3. 前端开发中的实际应用 💻
- 数据表格排序
js
/**
* 表格排序工具类
*/
class TableSorter {
constructor() {
this.sortStrategies = {
number: (a, b) => a - b,
string: (a, b) => a.localeCompare(b),
date: (a, b) => new Date(a) - new Date(b)
};
}
/**
* 对表格数据进行排序
* @param {Array} data - 表格数据
* @param {string} column - 排序列
* @param {string} type - 数据类型
* @param {boolean} ascending - 是否升序
*/
sort(data, column, type = 'string', ascending = true) {
const comparator = this.sortStrategies[type];
return [...data].sort((a, b) => {
const result = comparator(a[column], b[column]);
return ascending ? result : -result;
});
}
}
// 使用示例
const sorter = new TableSorter();
const tableData = [
{ name: 'John', age: 30, date: '2023-01-01' },
{ name: 'Alice', age: 25, date: '2023-02-01' }
];
// 按年龄排序
const ageSorted = sorter.sort(tableData, 'age', 'number', true);
- 文件列表排序
js
/**
* 文件排序工具类
*/
class FileSorter {
/**
* 按不同维度排序文件
* @param {Array} files - 文件列表
* @param {string} sortBy - 排序维度
*/
static sort(files, sortBy = 'name') {
const strategies = {
name: (a, b) => a.name.localeCompare(b.name),
size: (a, b) => a.size - b.size,
date: (a, b) => new Date(b.lastModified) - new Date(a.lastModified),
type: (a, b) => a.type.localeCompare(b.type)
};
return [...files].sort(strategies[sortBy]);
}
}
4. 性能优化建议 ⚡
通用优化
- 小数组使用插入排序
- 预处理数据减少比较成本
- 利用数据特征选择合适算法
前端特定优化
- 大数据量考虑分页排序
- 利用 Web Worker 处理排序
- 缓存排序结果
- 使用虚拟列表展示
5. 学习建议 📚
掌握基础
- 理解每种排序算法的核心思想
- 熟练手写快排和归并排序
- 理解稳定性的概念和应用
进阶提升
- 学习高级优化技巧
- 理解排序算法的取舍
- 结合实际场景练习
实战应用
- 多写demo
- 阅读开源代码
- 结合业务场景实践
6. 推荐资源 📖
在线练习平台
- LeetCode 排序专题
- 牛客网编程题
- CodeWars
推荐书籍
- 《算法》(第4版)
- 《JavaScript数据结构与算法》
- 《编程珠玑》
在线可视化工具
希望这篇文章能帮助你更好地理解和应用排序算法!如果有任何问题,欢迎讨论交流。 🎉