在JavaScript中,数组是一种常用的数据结构,而排序是处理数组的常见任务。对于JavaScript中的数组排序,我们可以通过多种方式来实现。本篇博客将详细介绍如何使用JavaScript实现数组排序功能,并分享一些感悟。
冒泡排序是一种简单的排序算法,它重复地遍历待排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
以下是一个简单的冒泡排序实现:
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) { // 如果前一个元素大于后一个元素,交换它们
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
快速排序是一种分而治之的排序算法。它首先选择一个“基准”元素,然后将所有小于基准的元素移动到其左侧,将所有大于基准的元素移动到其右侧。然后对左侧和右侧的元素再次进行快速排序。
以下是一个简单的快速排序实现:
function quickSort(arr) {
if (arr.length < 2) { return arr; } // 基本结束条件:如果数组只有一个元素,则已经排好序了
let pivot = arr[0]; // 选择第一个元素作为基准元素
let less = []; // 存放所有小于基准的元素
let greater = []; // 存放所有大于基准的元素
for (let i = 1; i < arr.length; i++) { // 将其他元素与基准进行比较,并放入相应的数组中
if (arr[i] < pivot) {
less.push(arr[i]);
} else {
greater.push(arr[i]);
}
}
return quickSort(less).concat(pivot, quickSort(greater)); // 对左侧和右侧的元素递归进行快速排序,并将结果连接起来形成最终的排序数组
}
插入排序的基本思想是将数组分为已排序和未排序两部分,初始时已排序部分包含一个元素,之后从未排序部分取出元素,并在已排序部分找到合适的插入位置插入,并保持已排序部分一直有序,重复此过程,直到未排序部分元素为空。
function insertionSort(arr) {
for (let i = 1; i < arr.length; 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;
}
选择排序的基本思想是每一次从未排序部分选择最小的元素,将其放到已排序部分的末尾,重复此过程,直到未排序部分元素为空。
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;
}
}
let temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
return arr;
}
归并排序是一种分治策略的排序算法,将数组分成两部分,分别对两部分进行排序,然后将有序的部分合并起来。这个过程递归进行,直到得到最终的有序数组。
function mergeSort(arr) {
if (arr.length < 2) { return arr; } // 基本结束条件:如果数组只有一个元素,则已经排好序了
let mid = Math.floor(arr.length / 2); // 找到中间点
let left = arr.slice(0, mid); // 分割数组为两部分
let right = arr.slice(mid); // 分割数组为两部分
return merge(mergeSort(left), mergeSort(right)); // 递归对左右两部分进行归并排序,并将结果合并起来形成最终的排序数组
}
function merge(left, right) { // 将两个已排序的数组合并成一个已排序的数组
let result = []; // 存放合并后的已排序数组的数组
while (left.length && right.length) { // 当左右两个数组都还有元素时,将较小的元素放入result数组中,并从相应的数组中删除该元素
if (left[0] < right[0]) {
result.push(left.shift()); // 将左侧元素放入result数组中,并从left数组中删除该元素
} else {
result.push(right.shift()); // 将右侧元素放入result数组中,并从right数组中删除该元素
}
} // 当其中一个数组的元素已经全部取出并放入result数组中时,将另一个数组剩下的元素直接全部放入result数组中(因为这些元素已经是已排序的了)
while (left.length) { result.push(left.shift()); } // 如果左数组还有元素,将其全部放入result数组中
while (right.length) { result.push(right.shift()); } // 如果右数组还有元素,将其全部放入result数组中
return result; // 返回合并后的已排序数组
}
当然,下面我将通过一个简单的例子来演示堆排序(Heap Sort)的过程。
假设我们有一个无序数组:[4, 10, 3, 5, 1]
。我们要将这个数组按照从小到大的顺序排列。在这个例子中,我们将使用小顶堆来进行排序。
首先,我们需要将数组转换成一个小顶堆。对于给定的数组,我们可以从最后一个非叶子节点开始,向上调整堆。
数组的初始状态(索引从0开始):
[4, 10, 3, 5, 1]
0 1 2 3 4
从索引 (n-2)/2
开始,这里是 (5-2)/2 = 1
,即元素 10
。但由于 10
已经是叶子节点,我们不需要调整它。接着我们考虑它的前一个元素,即索引为 0
的元素 4
。
调整后的堆(通过比较和交换):
[3, 10, 4, 5, 1] // 与子节点10比较,需要交换
0 1 2 3 4
[1, 10, 4, 5, 3] // 与子节点10和4比较,需要交换两次(先与4交换,再与10交换)
0 1 2 3 4
现在我们有了一个小顶堆。
接下来,我们将堆顶元素(最小值)与末尾元素交换位置:
[1, 10, 4, 5, 3] // 交换前
[3, 10, 4, 5, 1] // 交换后,将最小值1放到了末尾位置
0 1 2 3 4
交换后,我们移除了最小值 1
,并且将其放到了正确的位置上。现在剩下的数组部分 [3, 10, 4, 5]
不再是一个小顶堆,我们需要重新调整它。
重新调整剩余部分 [3, 10, 4, 5]
为小顶堆:
[3, 5, 4, 10] // 调整后的堆,通过与子节点比较和交换得到
0 1 2 3
注意,我们现在不考虑已经排序好的最后一个元素 1
。
继续这个过程,直到整个数组有序:
[3, 5, 4, 10] // 当前堆状态
[4, 5, 3, 10] // 交换堆顶和末尾元素
[4, 3, 5, 10] // 重新调整剩余部分为小顶堆
[3, 4, 5, 10] // 再次交换堆顶和末尾元素,现在数组已经有序了
最终我们得到了一个有序的数组 [1, 3, 4, 5, 10]
。需要注意的是,在实际执行过程中,我们不需要每次都重新构建整个堆,而只需要调整受影响的子堆部分。这里为了简单说明,我展示了整个数组的状态。在实际实现中,我们可以通过维护一个堆的数据结构来更有效地进行这些操作。
希尔排序是一种基于插入排序的算法,通过比较相距一定间隔的元素,使得在较远元素比较时能够克服插入排序的局限性。希尔排序的时间复杂度介于O(n^2)和O(n log n)之间,具体取决于间隔序列的选择。
下面是一个希尔排序的例子:
假设我们有一个无序数组:[69, 56, 12, 136, 3, 55, 46, 99, 88, 25]
。
首先,我们需要选择一个间隔序列。常见的间隔序列有 [n/2, n/4, n/8, ..., 1]
或 [3, 7, 15, ..., n/2]
等。在这里,我们选择 [n/2, n/4, n/8, ..., 1]
作为间隔序列。
接下来,我们按照间隔序列的顺序,对数组进行插入排序。
首先,取间隔 d=10/2=5
,将数组分成两组进行直接插入排序:
[69, 56, 12, 136, 3, 55, 46, 99, 88, 25] // 第一组
[25, 3, 12, 55, 56, 88, 99, 12, 136, 69] // 第二组
对每组分别进行插入排序:
[3, 12, 25, 55, 56, 69] // 第一组插入排序结果
[12, 25, 3, 55, 56, 88] // 第二组插入排序结果
接下来,取 d=d/2=2
,将数组分成两组进行直接插入排序:
[3, 12, 25, 55, 56, 69] // 第一组
[12, 25, 3, 55, 56, 88] // 第二组
对每组分别进行插入排序:
[3, 12, 25] // 第一组插入排序结果
[3, 12] // 第二组插入排序结果
最后,取 d=d/2=1
,将整个数组作为一个序列进行直接插入排序:
[3, 12, 25] // 进行一次插入排序后得到最终结果 [25, 12, 3]
最终我们得到了一个有序的数组 [25, 12, 3]
。可以看到,通过希尔排序,我们能够更快地将数组排序。
计数排序是一种非比较型整数排序算法,适用于正整数数组的排序。它通过统计每个元素出现的次数,将其按照出现次数放入新的数组中。计数排序在处理大数据集时具有较好的性能表现,但不适用于非整数或小数元素的排序。
以下是一个简单的计数排序的例子:
假设我们有以下整数数组:
arr = [4, 2, 7, 1, 5, 2]
首先,我们需要找到数组中的最大值和最小值,以确定计数数组的范围。在这个例子中,最小值为1,最大值为7。
然后,我们创建一个计数数组count,其长度为(最大值 - 最小值 + 1)。在这个例子中,计数数组长度为(7 - 1 + 1) = 7。
count = [0, 0, 0, 0, 0, 0, 0]
接下来,我们遍历原始数组,统计每个元素出现的次数,并在计数数组中相应位置累加。
计数数组count更新后为:
[1, 2, 0, 1, 1, 0, 1]
现在,我们可以根据计数数组还原排序后的数组。我们从计数数组中取出元素,其位置表示原始数组中的值,元素的值表示原始数组中该值出现的次数。然后,我们将这些值按照计数数组的顺序放回原始数组。
最终得到的排序后的数组为:
[1, 2, 2, 4, 5, 7]
这就是计数排序的基本过程。
基数排序是一种非比较型整数排序算法,适用于正整数或负整数的排序。它通过将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序在处理具有相同值元素的数组时具有较好的性能表现。
下面是一个基数排序的例子:
假设我们有一个整数数组:
arr = [170, 45, 75, 90, 802, 24, 2, 66]
首先,我们需要找到数组中最大数的位数,以便确定每个桶的个数。在这个例子中,最大数的位数是3位。
然后,我们创建一个长度为10的桶数组,每个桶用于存储一个位数的数字。桶的索引表示该位数的值,桶中的元素表示原始数组中该位数的数字。
接下来,我们按照从最低位到最高位的顺序遍历整数的每一位数,将每个数字放入相应的桶中。在这个例子中,我们先从个位开始。
个位上的数字有:[0, 2, 4, 6, 8, 9]
十位上的数字有:[1, 2, 4, 5, 7, 9]
百位上的数字有:[1, 2, 6, 8]
最后,我们将每个桶中的元素按照顺序合并起来,得到最终的排序结果:
[2, 24, 45, 66, 75, 90, 170, 802]
这就是基数排序的基本过程。注意,在实际应用中,我们需要根据具体情况选择合适的基数排序实现方式,例如选择从最高位开始排序还是从最低位开始排序,以及如何处理进位等情况。
在实现数组排序功能的过程中,我们分别介绍了冒泡排序和快速排序两种算法。冒泡排序虽然简单易懂,但效率较低,适用于较小的数组排序。而快速排序虽然实现起来稍显复杂,但其平均时间复杂度为O(nlogn),具有较好的性能表现,适用于较大的数组排序。在实际应用中,我们可以根据具体需求选择合适的排序算法。
通过这次实践,我深刻体会到算法在编程中的重要性。掌握多种算法不仅可以提高我们的编程能力,还可以让我们在处理问题时更加得心应手。同时,我也认识到算法的学习是一个不断深入的过程,需要我们不断地实践和总结。在未来的学习和工作中,我将继续努力深入学习算法,提高自己的编程能力。