-
Notifications
You must be signed in to change notification settings - Fork 0
Description
交换函数
function swap(arr, i, j) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
// es6
function swap(arr, i, j) {
[a[i], a[j]] = [a[j], a[i]];
}比较排序
冒泡排序
概念
比较前后两位数字的大小,如果前小后大,就交换位置,所以每次循环都能找出当前循环中最大的数字,推到最后面,然后每次循环减少最后一位(已经找出上一次循环中最大的数字,下一次就不用循环这个数字了)
代码实现
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - 1; j++) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j+1);
}
}
}优化方案
- 标记是否有序
- 有序区优化
- 鸡尾酒优化
选择排序
循环中找到最小的数字,跟最左边的数字进行交换
代码实现
for (let i = 0; i < arr.length - 1; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] > arr[j]) {
swap(arr, i, j);
}
}
}插入排序
概念
维护一个有序区,把元素一个一个插入到有序区的适当位置,直到所有元素有序
代码实现
for (var i = 0; i < arr.length; i++) {
var n = i;
while (arr[n] > arr[n + 1] && n >= 0) {
swap(arr, n , n+1)
n--;
}
}总结
这三者属于简单排序,平均时间复杂度都是 O(n2)
接近有序数组 插入排序 性能最好
无序时 选择排序性能最优
归并排序
概念
归并排序是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
代码实现
function mergeSort(arr) { //采用自上而下的递归方法
var len = arr.length;
if(len < 2) {
return arr;
}
var middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right)
{
var result = [];
console.time('归并排序耗时');
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while (left.length)
result.push(left.shift());
while (right.length)
result.push(right.shift());
console.timeEnd('归并排序耗时');
return result;
}总结
- 归并排序是稳定的排序
- 速度仅次于快速排序归并排序的比较次数小于快速排序的比较次数,移动次数一般多于快速排序的移动次数
把数组拆成两两一组,有序合并,并递归,经典的分治思想
快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 "基准"(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
/*
* 易于理解概念的版本
* 但是占用了额外的储存空间
* concat 操作也会增加复杂度
*/
var quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
const midIndex = Math.floor(arr.length / 2);
const mid = arr.splice(pivotIndex, 1)[0];
const left = [];
const right = [];
for (let i = 0; i < arr.length; i++){
if (arr[i] < mid) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return [...quickSort(left), mid, ...quickSort(right)];
};
// 不占用额外空间的版本
function partition(array, start, end) {
let j = start
let pivot = array[end]
for (let i = start; i <= end; i++) {
if (array[i] <= pivot) {
swap(array, i, j++)
}
}
return j - 1
}
function quickSort(array, start = 0, end = array.length -1) {
let pivotIndex = partition(array, start, end)
quickSort(array, start, pivotIndex - 1)
quickSort(array, pivotIndex + 1, end)
return array
}希尔排序
概念
希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
代码实现
function shellSort(arr) {
var len = arr.length,
temp,
gap = 1;
console.time('希尔排序耗时:');
while(gap < len/5) { //动态定义间隔序列
gap =gap*5+1;
}
for (gap; gap > 0; gap = Math.floor(gap/5)) {
for (var i = gap; i < len; i++) {
temp = arr[i];
for (var j = i-gap; j >= 0 && arr[j] > temp; j-=gap) {
arr[j+gap] = arr[j];
}
arr[j+gap] = temp;
}
}
console.timeEnd('希尔排序耗时:');
return arr;
}总结
- 不需要大量的辅助空间,和归并排序一样容易实现
- 希尔排序是基于插入排序的一种算法, 在此算法基础之上增加了一个新的特性,提高了效率
- 希尔排序的时间的时间复杂度为O( n3/2),希尔排序时间复杂度的下界是n*log2n
- 希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择,但是比O( )复杂度的算法快得多
堆排序
概念
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
代码实现
let len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
function buildMaxHeap(arr) {
// 建立大顶堆
len = arr.length;
// 从最后一个非叶子节点开始
for (var i = Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i);
}
}
// 堆调整
function heapify(arr, i) {
// 左节点
let left = 2 * i + 1;
// 右节点
let right = 2 * i + 2;
let largest = i;
// 存在左子节点且左子节点比当前节点大
if (left < len && arr[left] > arr[largest]) {
largest = left;
}
// 存在右子节点且右子节点比当前节点大
if (right < len && arr[right] > arr[largest]) {
largest = right;
}
if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest);
}
}
function heapSort(arr) {
// 先建立顶堆
buildMaxHeap(arr);
for (var i = arr.length - 1; i > 0; i--) {
// 交换首尾节点
swap(arr, 0, i);
// len-- 代表堆尾为排序完成的节点
len--;
// 对新堆进行排序
heapify(arr, 0);
}
return arr;
}总结
- 堆排序不是稳定排序
非比较排序
计数排序,基数排序,桶排序等非比较排序算法,平均时间复杂度都是O(n)。这些排序因为其待排序元素本身就含有了定位特征,因而不需要比较就可以确定其前后位置,从而可以突破比较排序算法时间复杂度O(nlgn)的理论下限。
计数排序
计数排序(Counting sort)是一种稳定的排序算法。计数排序是最简单的特例,由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存,适用性不高。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)
桶排序
桶排序假设输入是由一个随机过程产生,该过程将元素均匀、独立地分布在[0,1)区间上。
我们将[0,1)区间划分为n个相同大小的子区间,称为桶。然后将输入数据分别放到各个桶中。如果数据分布得很均匀,每个桶中的数据就不会太多,都会维持在常数量级。
基数排序
基数排序改善了计数排序,简单来说,基数排序算法就是将整数或字符串切分成不同的数字或字符,然后按对应位置的数或字符分别进行比较,这样就能将辅助数组或桶的数量降低到一个较小的值,经过多轮排序后得到最终的排序结果。