排序是一个比较基础的问题,所以排序算法的种类也比较多!下面我介绍下一些比较经典的排序算法及原理和复杂度分析!
一 、冒泡排序
冒泡排序就是重复“从序列右边开始比较相邻两个数字的大小(一定是相邻的两个数字)
,再根据结果交换两个数字的位置”这一操作的算法。在这个过程中,数字会像泡泡一样,慢慢从右往左“浮”到序列的顶端,因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样所以这个算法才被称为“冒泡排序”。当然你从左往右比较相邻两个数字的大小也是可以的,原理是一样的!
- 假如有个数组是[5, 9, 3, 1, 2, 8, 4, 7, 6],在序列的最右边放置一个天平,比较天平两边的数字。如果右边的数字较小,就交换这两个数字的位置。
- 由于6<7,所以交换这两个数字。
- 完成后,天平往左移动一个位置,比较两个数字的大小。此处4<6,所以无须交换。
4.继续将天平往左移动一个位置并比较数字。
- 重复同样的操作直到天平到达序列最左边为止。第一轮操作完成。
- 直到第n-1轮操作完成。
用代码来模拟实现:
//从右边(大)到左边(小)
let bubble = (arr) => {
for (let i = arr.length - 1; i > 0; i--) {
for (let j = arr.length - 1; j > arr.length - i - 1; j--) {
if (arr[j] < arr[j - 1]) {
//let temp = arr[j];
//arr[j] = arr[j - 1];
//arr[j - 1] = temp;
[arr[j], arr[j - 1]] = [arr[j - 1], arr[j]]
};
};
};
return arr;
};
//从左边(小)到右边(大)
let bubble = (arr) => {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
//let temp = arr[j];
//arr[j] = arr[j + 1];
//arr[j + 1] = temp;
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
};
};
};
return arr;
};
bubble([5, 9, 3, 1, 2, 8, 4, 7, 6])//[1, 2, 3, 4, 5, 6, 7, 8, 9]
复杂度分析:
在冒泡排序中,第1轮需要比较n-1次,第2轮需要比较n-2次……第n-1轮需要比较1次。因此,总的比较次数为(n-1)+(n-2)+…+1≈n2/2,因此,冒泡排序的时间复杂度为O(n2)
二 、选择排序
这个可能是大部分人最容易想到的排序方法!选择排序就是重复“从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换”这一操作的算法。在序列中寻找最小值时使用的是线性查找。
- 假如有个数组是[6, 1, 7, 8, 9, 3, 5, 4, 3],使用线性查找在数据中寻找最小值,于是我们找到最小值1。
- 将最小值1与序列最左边的6进行交换,最小值1归位。不过,如果最小值已经在最左端,就不需要任何操作。
- 在余下的数据中继续寻找最小值。这次我们找到了最小值2。
- 将数字2与左边第2个数字6进行交换,最小值2归位。
- 重复同样的操作直到所有数字都归位为止。
用代码来模拟实现:
let selectSort = (arr) => {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[i]) {
[arr[j], arr[i]] = [arr[i], arr[j]];
};
};
};
return arr;
};
selectSort([6, 1, 7, 8, 9, 3, 5, 4, 2]) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
复杂度分析:
选择排序使用了线性查找来寻找最小值,因此在第1轮中需要比较n-1个数字,第2轮需要比较n-2个数字……到第n-1轮的时候就只需比较1个数字了。因此,总的比较次数与冒泡排序的相同,都是(n-1)+(n-2)+…+1≈n2/2次。选择排序的时间复杂度也和冒泡排序的一样,都为O(n2)。
三、快速排序
快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将基准值(pivot)为中心点,比基准值小的数放基准值左边,比基准值大的数放基准值右边。
[比基准值小的数] 基准值 [比基准值大的数]
接着,对两个“[ ]”中的数据再重复进行上面快速排序,整体的排序便完成了。
- 假如有个序列数组是[3, 5, 8, 1, 2, 9, 4, 7, 6],在序列中随机选择一个基准值。这里选择了4。
- 将其他数字和基准值进行比较。小于基准值的往左移,大于基准值的往右移。
- 把基准值4插入序列。这样,4左边就是比它小的数字,右边就是比它大的数字。
- 两边的排序操作也和前面的一样。首先来看看如何对右边的数据进行排序吧,随机选择一个基准值。这次选择6。
- 把其余数据分别和基准值6进行比较,小于基准值的就往左移,大于的就往右移。
- 右边就和前面一样,先选出基准值,选择8作为基准值。
- 将9和7分别与基准值8进行比较后,两个数字的位置便分好了。8的两边都只有一个数据,因此不需要任何操作。这样7、8、9便完成排序了。
- 回到上一行,由于7、8、9完成了排序,所以5、6、7、8、9也完成了排序。
- 于是,最初选择的基准值4的右边排序完毕,再排序4左边的。
用代码来模拟实现:
let quickSort = (arr) => {
//如果arr的长度为1 时候直接输入arr
if (arr.length < 2) {
return arr;
};
//复制arr
let temp = arr.slice(0),
left = [],
right = [],
// middle = parseInt(temp.length / 2), 取中间值为基准值
middle = parseInt(temp.length * Math.random(1)),
pivot = temp[middle]; //随机取基准值
temp.splice(middle, 1); //删掉基准值
for (let i = 0; i < temp.length; i++) {
//比基准值大的放右边,比基准值小的放左边
temp[i] >= pivot ? right.push(temp[i]) : left.push(temp[i]);
};
//拼接 [left] [基准值] [right]
return quickSort(left).concat([pivot]).concat(quickSort(right));
}
quickSort([3, 5, 8, 1, 2, 9, 4, 7, 6]); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
快速排序还有其他的版本,因为上面每次调用quickSort 都会创建left,right数组,从而增加了空间复杂度,双指针版本正好解决了这个问题(不懂的可以单击这里快速排序原理和实现(图文讲解))
复杂度分析:
快速排序是一种“分治法”,每次都会将序列分成2半,将序列对半分割log2n次之后,那么总共会有log2n行,每行中每个数字都需要和基准值比较大小,因此每行所需的运行时间为O(n)。由此可知,整体的时间复杂度为O(nlog2n)/(nlogn)。如果运气不好,每次都选择最小值作为基准值,那么每次都需要把其他数据移到基准值的右边,递归执行n行,运行时间也就成了O(n2)。这就相当于每次都选出最小值并把它移到了最左边,这个操作也就和选择排序一样了。此外,如果数据中的每个数字被选为基准值的概率都相等,那么需要的平均运行时间为O(nlog2n)/(nlogn)。
四、归并排序
归并排序算法会把序列分成长度相同的两个子序列,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。该操作会一直重复执行,直到所有子序列都归并为一个整体为止。
-
假如有个序列为[6,4,3,7,5,1,2]。
-
首先,要把序列对半分割。
-
再继续往下分。
-
再继续往下分。
-
把6和4合并,合并后的顺序为[4, 6],接下来把3和7合并,合并后的顺序为[3, 7]。
-
看看怎么合并[4, 6]和[3, 7]。合并这种含有多个数字的子序列时,要先比较首位数字,再移动较小的数字。
-
由于4>3,所以移动3。
-
同样地,再次比较序列中剩下的首位数字,由于4<7,所以移动4。
9. 由于6<7,所以移动6,最后移动剩下的7。
-
右边同样执行上面的操作。然后比较两个子序列中的首位数字。
-
由于3>1,所以移动1。继续操作。
-
重复上面的操作直到合并完成,序列的排序也就完成了。
用代码来模拟实现:
let mergeSort = (arr) => {
//递归的条件是长度为1 跳出递归
if (arr.length < 2) {
return arr;
};
//将序列按中间分成left,right两份
let middle = parseInt(arr.length / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
};
let merge = (left, right) => {
let arr = [];
while (left.length && right.length) {
//每次都比较第一个数字
left[0] < right[0] ? arr.push(left.shift()) : arr.push(right.shift());
};
//一轮序列排序完成
return [...arr, ...left, ...right];
}
mergeSort([6, 4, 3, 7, 5, 1, 2]); //[1, 2, 3, 4, 5, 6, 7]
复杂度分析:
而将长度为n的序列对半分割直到只有一个数据为止时,可以分成log2n行,因此,总共有log2n行,无论哪一行都是n个数据,所以每行的运行时间都为O(n),所以总的运行时间为O(nlog2n)/(nlogn)
五、插入排序
插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。
- 假如有个序列为[5,3,4,7,2,8,6,9,1],首先,我们假设最左边的数字5已经完成排序,所以此时只有5是已归位的数字。
- 接下来,从待排数字(未排序区域)中取出最左边的数字3,将它与左边已归位的数字进行比较。若左边的数字更大,就交换这两个数字。
- 由于5>3,所以交换这两个数字。
- 接下来是第3轮。和前面一样,取出未排序区域中最左边的数字4,将它与左边的数字5进行比较。
5. 由于5>4,所以交换这两个数字。交换后再把4和左边的3进行比较,发现3<4,因为出现了比自己小的数字,所以操作结束。
6. 重复上述操作,直到所有数字都归位。
用代码来模拟实现:
let insertSort = (arr) => {
for (let j, i = 0; i < arr.length; i++, j = i) {
while (j--, j >= 0) {
if (arr[j] > arr[j + 1])
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
};
};
return arr;
};
insertSort([5, 3, 4, 7, 2, 8, 6, 9, 1]);// [1, 2, 3, 4, 5, 6, 7, 8, 9]
复杂度分析:
取出来的的数字和左边已归位的数字进行比较,第一轮比较0次,第二轮比较1次,第三轮比较2次,第n轮要比较n-1次,所以总的比较次数为(n-1)+(n-2)+…+1≈n2/2,因此,插入排序和冒泡排序的时间复杂度一样,都是为O(n2)
六 、希尔排序
希尔排序又称为缩小增量排序,它是一种插入排序,它是直接插入排序算法的加强版。把记录按步长分组,对每组记录采用直接插入排序方法进行排序,随着步长逐渐减小,所分成的组包含的记录越来越多,当步长的值减小到 1 时,整个数据合成为一组,构成一组有序记录,则完成排序。
-
假如有个序列为[7, 8, 4, 3, 9, 6, 1, 2]。
-
先设置步长(也就是两个数之间的间隔)为 length/2 =4。
-
[7,4]一组,[8,6]一组,[4,1]一组,[3,2]一组, 互相比较大小,然后交换位置,然后就变成了[4,7],[6,8],[1,4],[2,3]
-
第一轮结束了,接下来缩短步长,之前的步长/2(4/2=2)。
-
[7,1,9,4]一组,[6,2,8,3]一组,然后利用插入排序排成有序的序列[1,4,7,9],[2,3,6,8],看下图!
-
第二轮结束了,接下来再缩短步长,之前的步长/2(2/2=1)。
7. ,因为步长为1,所以成了一组[1,2,4,3,7,6,9,8],同样利用插入排序排成有序的序列[1,2,3,4,6,7,8,9],排序结束!
用代码来模拟实现:
let shellSort = (arr) => {
//第一次取长度的一半为步长
let step = Math.floor(arr.length / 2);
while (step >= 1) {
for (let i = 0; i < arr.length; i++) {
let j = i + step;
while (j < arr.length) {
//同一组的值相比较
if (arr[i] > arr[j])[arr[i], arr[j]] = [arr[j], arr[i]];
//如果长度不是偶尔,可能会出现一组3个,所以这里要再做处理
j += step;
};
};
//步长为之前的步长的一半
step = Math.floor(step / 2);
};
return arr;
};
shellSort([7, 8, 4, 3, 9, 6, 1, 2]); //[1, 2, 3, 4, 6, 7, 8, 9]
复杂度分析:
步长的选择是希尔排序的重要部分。希尔排序的时间复杂度受增量序列影响,不同的增量序列会有不同时间复杂度:N 为数组长度,K 为当前增量,当增量序列 k=2x 时,时间复杂度为O(N2)当增量序列 k=3(x+1) 时,时间复杂度为 O(n3/2),具体怎么算的可以自己去了解下!
参考资料:
- 浅谈排序算法(三):希尔排序
- 我的第一本算法书