再讲快排之前,首先对于任何一个数组,无论之前是多么杂乱,排完之后是不是一定存在一个数作为分界点(也就是所谓的支点),在支点左边全是小于等于这个支点的,然后在这个支点右边的全是大于等于这个支点的,快排过程就是寻找这个支点过程
先看普通的快排(普通单路快排)
代码如下
let findIndex = (arr, l, len) => { let par = arr[l], j = l for (let i = l + 1; i <= len; i++) { if (arr[i] < par) { swap(arr, ++j, i) } } swap(arr, j, l) return j } let _quick = (arr, l, len) => { if (l >= len) { return } let p = findIndex(arr, l, len) _quick(arr, l, p) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
这是一个普通单路快排实现的代码,如果是一般杂乱的数组,测试之后这个代码的运行时间是很短的,但是这里存在一个问题,就是如果我待排序的数组是一个顺序性很大数组(比如[1,2,3,4,5,6,7]),那么这个代码将会退化到O(n²)级别,为什么?
首先快排是个遍历下个数字并确定支点过程,首先先假设第一个就是支点,那么后面的书如果比支点大,说明支点不需要移动(因为右边统一比支点大),如果后面的数比支点小,说明这个数字应该在支点前面,对不对,这个时候支点实质上应该向左移动一位(因为之前那个位置让给比他小的那个数字了,注意这里实质上,因为本轮排序没结束,还没有找到支点应该在的准确位置,所以支点还是第一个),然后将加1后支点所在位的当前数字和当前数交换(因为新的位置已经被支点占据了,而原支点位置是比支点小的数字),依次类推,最后找到所有混乱数组里面,最后一个小于支点的数字,统计出所有小于支点的总数是k,那么这个k就是支点应该在这个混乱数组里的具体位置!然后再依此支点为分界点,递归排序
ok,那么上面代码存在什么问题呢?假设待排序数组(比如[1,2,3,4,5,6,7]),默认取第一个,可是往后面遍历的时候,后面数字全是大于1的,第一轮循环结束,时间复杂度n,再取第二个2,结果发现后面的又是全大于2的,依次循环,不难发现用上述代码是n²的复杂度
我们无法100%完全避免这种退化现象的,但是我们可以尽量避免。看下面随机单路快排代码
let findIndex = (arr, l, len) => { let idx = Math.floor(Math.random() * (len -l) + l) swap(arr, l, idx) let par = arr[l], j = l for (let i = l + 1; i <= len; i++) { if (arr[i] < par) { swap(arr, ++j, i) } } swap(arr, j, l) return j } let _quick = (arr, l, len) => { if (l >= len) { return } let p = findIndex(arr, l, len) _quick(arr, l, p) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
这个时候,每次虽然仍然是取第一位作为支点,但是呢,我们的支点是经过随机化处理的,也就是说如果有n个数字,第一次正好取到最小的,概率是1/n,第二次又正好是最小的也就是1/n-1,可以这样处理让快排退化的概率是很低的,当然如果真的出现了那种情况,那只能认吧,因为快排本身是期望复杂度O(log2N),这是我们的期望值
乍一看,似乎现在随机快排已经很不错了,是的吗?
乍一看似得,可是假设我们的待排序数组是一个有许多重复数值的数组呢?比如[4,2,2,2,3,6,5],那么我们数组又将会分成两个不平衡的两部分,怎么避免,双路快排登场
let findIndex = (arr, l, r) => { swap(arr, l, Math.floor(Math.random() * (r - l + 1) + l)) let j =r, i = l + 1 let begin = arr[l] // [l+1, i), (j, r] while (i <= j) { while (i <= r && arr[i] < begin) { i++ } while (j >= l + 1 && arr[j] > begin) { j-- } swap(arr, i++, j--) } swap(arr, l, j) return j } let insert = (arr, l, end) => { for (let i = l + 1; i <= end; i++) { let e = arr[i] let j for (j = i; j > 0 && e < arr[j - 1]; j--) { arr[j] = arr[j - 1] } arr[j] = e } return arr } let _quick = (arr, l, len) => { if (l >= len - 15) { return insert(arr, l, len) } let p = findIndex(arr, l, len) _quick(arr, l, p - 1) _quick(arr, p + 1, len) } let quick = (arr) => { let len = arr.length _quick(arr, 0, len - 1) return arr }
注意双路快排指针还是一个,但是是从两边夹攻,他的结果就是即使你是和指针相等的,我也交换,这样就避免了,不平衡的出现,我们的快排又回到O(nlog2N)的时间复杂度
相比较双路快排是找大于或者等于对应位置,三路快排是说我找的是一个区间,找的是一个等于指针的那个区间