排序算法算是面试中比较高频的问题了
排序算法有很多种,比如:
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- ...
每种排序都有各自的优势
今天我们来讲快速排序
它的优势就是它的名字,速度快,时间复杂度大概是
实现
快速排序的实现:
- 将元素分割成独立的两部分,左边的元素均比右边的小
- 将这两部分分别再次执行第1步,然后一直分割下去,直到不可分割为止
分割
我们首先来实现第一步,怎么分割才能实现左小右大呢?
我们从中取一个元素,比这个元素小放左边,比这个元素大就放右边不就行了么:
function partition(arr) {
var pivot = 0,
index = pivot + 1;
for (var i = index; i <= arr.length - 1; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
}
function swap(arr, i ,j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
上面简单实现了一个分割函数partition,代码比较晦涩难懂,我们举个例子:
将数组[8, 14, 3, 5, 17]传入partition
首先进行初始化:
pivot:译为基准,也就是我们需要比较的元素,比它小的元素放左边,比它大的元素放右边,这里指向的是数组的第一个元素8index:慢指针,指向pivot的下一个元素,如果i遍历到的元素比pivot小,index和i的元素互换,然后index++指针右移一位指向下一个元素i:快指针,指向index,从index开始遍历到数组末尾,遇到比pivot小的,i和index的元素互换
初始化后是这样的:
i遍历到3时,3比8小,互换index和i的元素,然后index++
i遍历到5时,5比8小,互换index和i的元素,然后index++
此时i已遍历完毕,执行swap(arr, pivot, index - 1),互换pivot和index-1的元素
最终变成了这样:
此时比8小的元素放在左边,比8大的元素放在右边,完美实现了分割
这里用到了双指针算法里非常经典的快慢指针
递归分割
此时我们实现了分割函数,现在我们需要将分割的两部分再次分割,然后一直分割下去,直到不可分割为止
这里我们需要用递归来执行,下面是一个完整的快速排序函数quickSort:
function quickSort(arr, left, right) {
var len = arr.length,
partitionIndex,
left =typeof left !='number' ? 0 : left,
right =typeof right !='number' ? len - 1 : right;
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
}
return arr;
}
function partition(arr, left, right) {
var pivot = left,
index = pivot + 1;
for (var i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
return index-1;
}
function swap(arr, i ,j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- 设置
left和right为要分割的区间,初次为整个数组 - 执行
partition分割整个数组 - 执行完后返回
partitionIndex,它指向基准值(比它小的排左边,比它大的排右边) - 根据
partitionIndex分割为左右两个区间,然后两个区间再次递归执行quickSort,然后再次执行里面的partition再次分割(根据left和right定义区间) - 这样一直递归执行
quickSort,直到不可分割为止(left < right)
总结
至此整个数组就排完序了
下面的动图帮助大家更好的理解:
时间复杂度
快速排序的时间复杂度大概是,这个怎么来的呢?
首先递归执行quickSort类似于一个二叉树的遍历,时间复杂度是
然后每次执行执行partition时,内部都会有一个for循环遍历,for循环遍历的时间复杂度是
两者内外嵌套在一起,时间复杂度就是
不稳定
快速排序的时间复杂度是,但有时候会暴涨到
为什么呢?
如果数组是有序的,比如[1, 2, 3, 4, 5, 6, 7, 8, 9],那么:
- 取到1进行分割,比1小的分到左边,比1大的分到右边,发现左边没有元素,右边是剩余的全部元素
- 左边没有元素就不用分割了,接着分割右边的元素
- 递归分割下去
之前的情况是左右都有元素,同时分割数组一半的长度,时间复杂度是
这里是只有右边有元素,而且是整个数组长度,那这样的话遍历整个数组的时间复杂度就是,加上内部的for循环时间复杂度就是
所以如果数组有序的话快速排序的时间复杂度反而慢了