前言
前面已经介绍的几种简单的排序算法,在正式进入高级排序算法之前,我们再回顾一下插入排序的主要思想:提取一个标记元素,它左边的数据构成局部有序的序列,右边的数据为无序序列,再讲无序序列的每一个元素插入到有序序列的相应位置中。
正文
回顾
假如有这样一个序列:
当排列到最后一个元素1的时候,是以下的结构:
那最后的一趟我们需要移动所有的元素才能将元素1放入到相应的位置
这个就是问题所在。怎样让1能够往前一些,后面就不用移动这么多步呢?那就引入了希尔排序。
希尔排序
希尔排序的改进突破了前面排序算法的时间复杂度为 O(N2)。
思想
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。
- 第一步,将下标值为一定增量分组,(一般是N/ 2,后续连续减一半)按照插入排序的方式对每一组进行排序。
- 第二步,增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
例子
选择合适的增量
在前人的探索当中,有几个比较出名的增量序列。
- 希尔原稿增量序列:在希尔排序的原稿当中,建议初始间距为 N / 2 ,每一趟的间距都分成两半,比如在N= 100 的数组里面,增量间隔序列就会是:50,25,12,6,3,1
- Hibbard增量序列:增量算法是2 k-1,就是1,3,5,7…等。最坏复杂度为O(N3/2),猜想平均复杂度为O(N5/4)。
- Sedgewick增量序列:增量算法是4i - 32i + 1 ,就是1,5,19,41,109…等。
最坏复杂度为O(N4/3),平均复杂度为O(N7/6)。
Hibbard增量序列Sedgewick增量序列的复杂度还暂未被证实,而且也比较难实现,因此我们选择希尔原稿的增量序列,这种方法不需要在开始排序前进行额外的计算工作。
希尔排序算法实现
// 希尔排序
ArrayList.prototype.sellSort = function () {
// 1.获取数组的长度
var length = this.array.length;
// 2.选择希尔排序的原稿增量,初始间距是N / 2
var gap = Math.floor(length / 2);
//3.让间隔gap不断的减小
while (gap >= 1) {
// 4.以grp作为间隔,进行分组,分组进行插入排序
for (var i = gap; i < length; i++) {
var temp = this.array[i];
var j = i;
while (this.array[j - gap] > temp && j > gap - 1) {
this.array[j] = this.array[j - gap];
j -= gap;
}
// 5.将j位置的元素赋值给temp
this.array[j] = temp;
}
// 6.重新计算新的间隔
gap = Math.floor(gap / 2);
}
}
希尔排序的效率
时间复杂度为O(N2)
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
快速排序
前面说的希尔排序是对插入排序的一种改进,而快速排序是对冒泡排序的升级版,其实比冒泡排序改进了很多,它运用了递归。
思想
- 第一步:确定一个基准;
- 第二步:将所有小于基准的数放到基准的前面,所有大于基准的数放到基准的后面(相同的数可放到任意一边)。这个分区之后,该基准位于数列的偏中间位置,这个过程叫做分区操作。
- 递归地把基准的前与后数列进行第一和第二步操作,直到排序完毕。
选择合适的基准
-
- 选择部分序列的最后一个数据作为基准,后续使用两个移动的指针,如果前指针的数比后指针的数大则交换他们两。(操作简单,但不具代表性)
-
- 在数据范围内使用随机索引作为基准,将基准放置部分序列最后,后与第一种方法操作相同。(概率相等,但需要实现随机数,无统一操作)
-
- 选择部分序列的前中后三个数据排序后取中位数,将中位数作为基准,放置序列的倒数第二个位置。(具有代表性,统一操作)
综合的考虑我们最后选择取中位数的方法
快速排序算法实现
2、交换两数
// 交换两个数
ArrayList.prototype.swap = function (a, b) {
var temp = this.array[a];
this.array[a] = this.array[b];
this.array[b] = temp;
}
1、确定基准
// 确定基准
ArrayList.prototype.median = function (left, right) {
// 1.求出中间的位置
var mid = Math.floor((left + right) / 2);
// 2.判断并且进行交换 三数的冒泡思想
if (this.array[left] > this.array[mid]) this.swap(left, mid);
if (this.array[left] > this.array[right]) this.swap(left, right);
if (this.array[mid] > this.array[right]) this.swap(mid, right);
// 3.巧妙的操作: 将mid移动到right - 1的位置.
this.swap(mid, right - 1);
// 4.返回pivot
return this.array[right - 1];
}
// 快速排序
ArrayList.prototype.quickSort = function () {
this.quick(0, this.array.length - 1);
}
// 内部递归使用
ArrayList.prototype.quick = function (left, right) {
// 1.递归结束条件
if (left >= right) return false;
// 2.获取基准
var pivot = this.median(left, right);
// 3.定义变量,开始进行交换
var i = left;
var j = right - 1;
while (i < j) {
// 3.1 找到比基准值大的数停止
while (i < right && this.array[++i] < pivot) {
}
// 3.2 找到比基准值小的数停止
while (j > left && this.array[--j] > pivot) {
}
// 3.3 交换与否
if (i < j) {
this.swap(i, j);
} else {
break;
}
}
// 4.将基准放在正确的位置
this.swap(i, right - 1);
// 5.递归的调用左边序列
this.quick(left, i - 1);
// 6.递归的调用右边序列
this.quick(i + 1, right);
}
百度百科精简版
const quickSort = (array) => {
const sort = (arr, left = 0, right = arr.length - 1) => {
if (left >= right) {
//如果左边的索引大于等于右边的索引说明整理完毕
return
}
let i = left
let j = right
const baseVal = arr[j] // 取无序数组最后一个数为基准值
while (i < j) {
//把所有比基准值小的数放在左边大的数放在右边
while (i < j && arr[i] <= baseVal) {
//找到一个比基准值大的数交换
i++
}
arr[j] = arr[i] // 将较大的值放在右边如果没有比基准值大的数就是将自己赋值给自己(i 等于 j)
while (j > i && arr[j] >= baseVal) {
//找到一个比基准值小的数交换
j--
}
arr[i] = arr[j] // 将较小的值放在左边如果没有找到比基准值小的数就是将自己赋值给自己(i 等于 j)
}
arr[j] = baseVal // 将基准值放至中央位置完成一次循环(这时候 j 等于 i )
sort(arr, left, j - 1) // 将左边的无序数组重复上面的操作
sort(arr, j + 1, right) // 将右边的无序数组重复上面的操作
}
const newArr = array.concat() // 为了保证这个函数是纯函数拷贝一次数组
sort(newArr)
return newArr
}
希尔排序的效率
时间复杂度为O(N*logN)
总结
只学习了这两种比较快的排序算法,桶排序和归并排序还没学习,等学了在来分享嘻嘻。一起加油ヾ(◍°∇°◍)ノ゙。