写在前面

一直很惧怕算法,总是感觉特别伤脑子,因此至今为止,几种基本的排序算法一直都不是很清楚,更别说时间复杂度、空间复杂度什么的了。

今天抽空理了一下,其实感觉还好,并没有那么可怕,虽然代码写出来还是磕磕绊绊,但是思想和原理还是大致上摸清楚了,记录、分享。

说明

关于排序,前辈们已经讲解的够多了,我这里主要摘录一些概念。

排序算法分类

排序稳定性

排序算法稳定性的简单形式化定义为:如果Ai = Aj,排序前Ai在Aj之前,排序后Ai还在Aj之前,则称这种排序算法是稳定的。

选择排序

选择排序每次比较的是数组中特定索引的值与全数组中每个值的大小比较,每次都选出一个最小(最大)值,如果当前索引的值大于之后索引的值,则两者进行交换

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(n^2)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var temp;

for (var i = 0; i < arr.length; i++) {
	for (var j = i + 1; j < arr.length; j++) {
		if (arr[i] > arr[j]) {
			temp = arr[j];
			arr[j] = arr[i];
			arr[i] = temp;
		}
	}
}

console.log(arr);

过程大致如下:

1 4 5 2 3 9 0 7 6
0 4 5 2 3 9 1 7 6
0 2 5 4 3 9 1 7 6
0 1 5 4 3 9 2 7 6
0 1 4 5 3 9 2 7 6
0 1 3 5 4 9 2 7 6
0 1 2 5 4 9 3 7 6
0 1 2 4 5 9 3 7 6
0 1 2 3 5 9 4 7 6
0 1 2 3 4 9 5 7 6
0 1 2 3 4 5 9 7 6
0 1 2 3 4 5 7 9 6
0 1 2 3 4 5 6 9 7
0 1 2 3 4 5 6 7 9

冒泡排序

冒泡排序每次从数组的最开始索引处与后一个值进行比较,如果当前值比较大,则交换位置。这样一次循环下来,最大的值就会排入到最后的位置。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- 如果能在内部循环第一次运行时,使用一个旗标来表示有无需要交换的可能,可以把最优时间复杂度降低到O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var t;

for (var m = 0; m < arr.length; m++) {
	for (var n = 0; n < arr.length - m; n++) {
		if (arr[n] > arr[n + 1]) {
			t = arr[n + 1];
			arr[n + 1] = arr[n];
			arr[n] = t;
		}
	}
}

console.log(arr);

过程大致如下:

1 4 5 2 3 9 0 7 6
1 4 2 5 3 9 0 7 6
1 4 2 3 5 9 0 7 6
1 4 2 3 5 0 9 7 6
1 4 2 3 5 0 7 9 6
1 4 2 3 5 0 7 6 9
1 2 4 3 5 0 7 6 9
1 2 3 4 5 0 7 6 9
1 2 3 4 0 5 7 6 9
1 2 3 4 0 5 6 7 9
1 2 3 0 4 5 6 7 9
1 2 0 3 4 5 6 7 9
1 0 2 3 4 5 6 7 9
0 1 2 3 4 5 6 7 9

插入排序

插入排序类似于扑克牌的插入方法,选取待排列数组中的任意一个数字作为已排序的基准,再依次从待排序数组中取出数字,根据依次比较,将这个数字插入到已排序的数组中

// 分类 ------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 最坏情况为输入序列是降序排列的,此时时间复杂度O(n^2)
// 最优时间复杂度 ---- 最好情况为输入序列是升序排列的,此时时间复杂度O(n)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

/**
 * 直接使用同一个数组方式
 */
for (var i = 1; i < arr.length; i++) {
	var get = arr[i];
	var j = i - 1;
	// 倒叙比较已经排序的值和取到的值进行比较
	// 如果取到的值在已经排序中的值中存在合适的索引插入,则需要将这个索引之后的值进行后移
	while (j >= 0 && arr[j] > get) {
		arr[j + 1] = arr[j];
		j--;
	}
	arr[j + 1] = get;
}
console.log(arr);

/**
 * 引入一个新的数组方式
 * 引入一个数组后会更好理解
 */
var sortList = [arr[0]];

for (var i = 1; i < arr.length; i++) {
	var sLen = sortList.length;

    // 如果取出的数字比已经排序的第一个值都小,则插入到最开始
	if (arr[i] < sortList[0]) {
		sortList.unshift(arr[i])
		continue;
	}

    // 如果取出的数字比已经排序的最后一个值都大,则插入到最末尾
	if (arr[i] > sortList[sLen - 1]) {
		sortList[sLen] = arr[i];
		continue;
	}

	for (var j = 0; j < sLen - 1; j++) {
		if (arr[i] >= sortList[j] && arr[i] <= sortList[j + 1]) {
			sortList.splice(j + 1, 0, arr[i]);
			break;
		}		
	}
}

console.log(sortList);

过程大致如下:

1
1 4
1 4 5
1 2 4 5
1 2 3 4 5
1 2 3 4 5 9
0 1 2 3 4 5 9
0 1 2 3 4 5 7 9
0 1 2 3 4 5 6 7 9

二分插入排序

二分插入排序是直接插入排序的一个变种,利用二分查找法找出下一个插入数字对应的索引,然后进行插入。

当n较大时,二分插入排序的比较次数比直接插入排序的最差情况好得多,但比直接插入排序的最好情况要差,所当以元素初始序列已经接近升序时,直接插入排序比二分插入排序比较次数少。二分插入排序元素移动次数与直接插入排序相同,依赖于元素初始序列。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(n^2)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(n^2)
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

/**
 * 直接使用同一个数组方式
 */
for (var i = 1; i < arr.length; i++) {
	var get = arr[i];
	var left = 0;
	var right = i - 1;

    // 每次找出中间位置然后进行比较,最终确定索引位置
	while (left <= right) {
		var mid = parseInt((left + right) / 2);
		if (arr[mid] > get) {
			right = mid - 1;
		} else {
			left = mid + 1;
		}
	}
    
    for (var k = i - 1; k >= left; k--) {
        arr[k + 1] = arr[k];
    }
    arr[left] = get;
	
}

/**
 * 引入一个新的数组方式
 * 引入一个数组后会更好理解变化的方式
 */
var sortList = [arr[0]];

for (var i = 1; i < arr.length; i++) {
	var sLen = sortList.length;
	var get = arr[i];
	var left = 0;
	var right = sLen - 1;

    // 每次找出中间位置然后进行比较,最终确定索引位置
	while (left <= right) {
		var mid = parseInt((left + right) / 2);
		if (sortList[mid] > get) {
			right = mid - 1;
		} else {
			left = mid + 1;
		}
	}

    // splice是数组插入值的一个快捷方式,将值移位的方式如下
    // sortList.splice(left, 0, get);
    
    for (var k = sLen - 1; k >= left; k--) {
        sortList[k + 1] = sortList[k];
    }
    sortList[left] = get;
	
}

console.log(sortList);

过程大致如下:

1
1 4
1 4 5
1 2 4 5
1 2 3 4 5
1 2 3 4 5 9
0 1 2 3 4 5 9
0 1 2 3 4 5 7 9
0 1 2 3 4 5 6 7 9

希尔排序

希尔排序是一种更高效的插入排序,通过设计步长(gap)将数组分组,然后每组中单独采用排序算法将每组排序,然后在缩小步长,进行重复的分组排序工作,直到gap变为1的时候,整个数组分为一组,算法结束。

例如:数组 [1, 4, 5, 2, 3, 9, 0, 7, 6],如果每次以数组长度的一半来作为步长,可以分解为以下步骤

1. gap: Math.floor(9 / 2) = 4;

分为四组,分组为: 
{ 1, 3 }, { 4, 9 }, { 5, 0 }, { 2, 7 }

最后一个数字 6 需要等到第5个数字排序完成,也就是3,可以得出3依旧还处在第4索引的位置,因此最后一个分组为 { 3, 6 }

完成一轮分组以及排序后的数组为:[ 1, 4, 0, 2, 3, 9, 5, 7, 6 ]

2. gap: Math.floor(4 / 2) = 2;

分为两组,分组为:
{ 1, 0, 3, 5, 6 }, { 4, 2, 9, 7 }

完成第二轮分组以及排序后的数组为:[ 0, 2, 1, 4, 3, 7, 5, 9, 6 ]

3. gap: Math.floor(2 / 2) = 1;

分为一组,即为:{ 0, 2, 1, 4, 3, 7, 5, 9, 6 }

完成第三轮分组以及排序后的数组为:[ 0, 1, 2, 3, 4, 5, 6, 7, 9 ]
// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
// 最优时间复杂度 ---- O(n)
// 平均时间复杂度 ---- 根据步长序列的不同而不同。
// 所需辅助空间 ------ O(1)
// 稳定性 ------------ 不稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var gap = Math.floor(arr.length / 2);

function swap(arr, i, j) {
	var t;
	t = arr[j];
	arr[j] = arr[i];
	arr[i] = t;
}

for (; gap > 0; gap = Math.floor(gap / 2)) {
	//从第gap个元素,逐个对其所在组进行直接插入排序操作
	for(var i = gap; i < arr.length; i++) {
		var j = i;
		// 这里采用的其实是冒泡排序
		while(j - gap >= 0 && arr[j] < arr[j-gap]) {
			//插入排序采用交换法
			swap(arr, j, j-gap);
			j -= gap;
		}
		
		// 或者插入排序
		var temp = arr[j];
		if (arr[j] < arr[j-gap]) {
		    while (j-gap >= 0 && temp < arr[j-gap]) {
		        arr[j] = arr[j-gap];
		        j -= gap;
		    }
		    arr[j] = temp;
		}
	}
}

console.log(arr);

过程大致如下:

1 4 5 2 3 9 0 7 6
1 4 0 2 3 9 5 7 6
0 4 1 2 3 9 5 7 6
0 2 1 4 3 9 5 7 6
0 2 1 4 3 7 5 9 6
0 1 2 4 3 7 5 9 6
0 1 2 3 4 7 5 9 6
0 1 2 3 4 5 7 9 6
0 1 2 3 4 5 7 6 9
0 1 2 3 4 5 6 7 9

归并排序

归并排序采用的是一种分治思想,将整个数组递分成若干小组,直到最后组中的个数为1时停止,那么此时再与同一级别的分组数字进行比较,这就是的操作。然后向上一层层地进行合并,最终合成一个排序好的数组。

这么讲可能有点糊涂,用一个例子分析。比如现在有这两个排序好的数组

var a = [1, 4, 6, 7, 9];
var b = [2, 3, 5, 8];
var temp = [];

// 比较过程如下:
// 比较两个数组中的第一个数字,将数字小的压进temp数组,同时将这个数字从原数组中删除

// 第一步
a[0] < b[0] 
// 得到
a: [4, 6, 7, 9]
b: [2, 3, 5, 8]
temp: [1]

// 第二步
a[0] > b[0]
// 得到
a: [4, 6, 7, 9]
b: [3, 5, 8]
temp: [1, 2]

// 第三步
a[0] > b[0]
// 得到
a: [4, 6, 7, 9]
b: [5, 8]
temp: [1, 2, 3]

// 中间省略N步

// 第N+1步
a: [9]
b: []
temp: [1, 2, 3, 4, 5, 6, 7, 8]
// 此时b数组已经为空,则直接归并
// 得到
a: []
b: []
temp: [1, 2, 3, 4, 5, 6, 7, 8, 9]

注:以上的步骤只是归并排序递归中的最上层的一步,其中下面还会分成很多小的合并步骤。

// 分类 -------------- 内部比较排序
// 数据结构 ---------- 数组
// 最差时间复杂度 ---- O(nlogn)
// 最优时间复杂度 ---- O(nlogn)
// 平均时间复杂度 ---- O(nlogn)
// 所需辅助空间 ------ O(n)
// 稳定性 ------------ 稳定

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var len = arr.length;

function mergeArray(arr, first, mid, last, t) {
	var i = mid, 
		j = last,
		m = first,
		n = mid + 1,
		k = 0;

	while (m <= mid && n <= last) {
		if (arr[m] > arr[n]) {
			t[k++] = arr[n++];
		} else {
			t[k++] = arr[m++];
		}
	}

	while (m <= i) {
		t[k++] = arr[m++]
	}

	while(n <= j) {
		t[k++] = arr[n++];
	}

	for (var p = 0; p < k; p++) {
		arr[first + p] = t[p];
	}
}

function mergeSort(arr, first, last, t) {
	if (first < last) {
		var mid = Math.floor((first + last) / 2);
		mergeSort(arr, first, mid, t);
		mergeSort(arr, mid + 1, last, t)
		mergeArray(arr, first, mid, last, t);
	}
}

mergeSort(arr, 0, len - 1, []);

console.log(arr);

过程大致如下:

1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 4 5 2 3 9 0 7 6
1 2 3 4 5 9 0 7 6
1 2 3 4 5 0 9 7 6
1 2 3 4 5 0 9 6 7
1 2 3 4 5 0 6 7 9
0 1 2 3 4 5 6 7 9

快速排序

快速排序的原理是:首先随机选择一个值,遍历整个数组,比这个值小的放在左边的数组中,比这个值大的放在右边的数组中,然后再根据上一步得出的左右数组重复上述的操作,直到分出的左右数组长度为1或者0的时候停止。

还是举个栗子吧:


var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

// 1. 选取一个数,我这里取中间的数,即为arr[4] = 3
left: [1, 2, 0]
right: [4, 5, 9, 7, 6]

// 2. 在左右数组中重复上述操作
left: [1, 2, 0]
取数:left[1] = 2

left-left: [0, 1]   // 继续递归
left-right: []      // 递归结束,直接返回

right: [4, 5, 9, 7, 6]
取数: right[3] = 9
right-left: [4, 5, 7, 6]    // 继续递归
right-right: []             // 递归结束,直接返回

在递归中排序,然后连接选出的那个数,就完成了整个数组的排序

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];

function quickSort(arr) {
	if (arr.length === 1 || arr.length === 0) {
		return arr;
	}

	var left = [];
	var right = [];
	var len = arr.length;
	var f = 0;
	var l = len - 1;
	var mid = Math.floor((f + l) / 2);
	var midVal = arr[mid];

	for (var i = 0; i < len; i++) {
		if (arr[i] < arr[mid]) {
			left.push(arr[i]);
		} else if (arr[i] > arr[mid]) {
			right.push(arr[i])
		}
	}

	var leftArr = quickSort(left);
	var rightArr = quickSort(right);

	return leftArr.concat(midVal).concat(rightArr);
}

var result = quickSort(arr);
console.log(result);

大致过程如下:

left:    1 2 0
middle:  3
right:   4 5 9 7 6

left:    1 0
middle:  2
right:  

left:    0
middle:  1
right:  

left:    4 5 7 6
middle:  9
right:  

left:    4
middle:  5
right:   7 6

left:    6
middle:  7
right:  

堆排序

堆排序是指利用堆这种数据结构所设计的一种选择排序算法。堆是一种近似完全二叉树的结构(通常堆是通过一维数组来实现的),并满足性质:以最大堆(也叫大根堆、大顶堆)为例,其中父结点的值总是大于它的孩子节点。

我们可以很容易的定义堆排序的过程:

更多请参看https://www.cnblogs.com/skywang12345/p/3602162.html,这篇文章中进行了很详细地讲解。

var arr = [1, 4, 5, 2, 3, 9, 0, 7, 6];
var len = arr.length;

function swap(arr, i, j) {
    var t = arr[j];
    arr[j] = arr[i];
    arr[i] = t;
}

function heapAdjust(arr, i, end) {
	var left = 2 * i + 1;				// 左边子节点
	var right = 2 * i + 2;				// 右侧子节点
	var max = i;

	if (left < end && arr[left] > arr[max]) {
		max = left;
	}

	if (right < end && arr[right] > arr[max]) {
		max = right;
	}

	if (max !== i) {
		swap(arr, max, i);
		heapAdjust(arr, max, end);
	}
}

function buildMaxHeap(arr, len) {
	var sNode = Math.floor(len / 2) - 1;	// 第一个需要调整的非叶子节点
	for (var i = sNode; i >= 0; i--) {
		heapAdjust(arr, i, len);
	}
	return len;
}

function heapSort(arr) {
	var heapSize = buildMaxHeap(arr, len);

	// 堆(无序区)元素个数大于1,未完成排序
	while (heapSize > 1) {
        // 将堆顶元素与堆的最后一个元素互换,并从堆中去掉最后一个元素
        // 此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        swap(arr, 0, --heapSize);
        // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
        heapAdjust(arr, 0, heapSize);     
    }
}

heapSort(arr);
console.log(arr);

大致实现如下:

1 4 5 2 3 9 0 7 6
7 6 5 4 3 1 0 2 9
6 4 5 2 3 1 0 7 9
5 4 1 2 3 0 6 7 9
4 3 1 2 0 5 6 7 9
3 2 1 0 4 5 6 7 9
2 0 1 3 4 5 6 7 9
1 0 2 3 4 5 6 7 9
0 1 2 3 4 5 6 7 9

参考