快排,随机快排,双路快排,三路快排的理解

Posted 夜皇帝

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快排,随机快排,双路快排,三路快排的理解相关的知识,希望对你有一定的参考价值。

再讲快排之前,首先对于任何一个数组,无论之前是多么杂乱,排完之后是不是一定存在一个数作为分界点(也就是所谓的支点),在支点左边全是小于等于这个支点的,然后在这个支点右边的全是大于等于这个支点的,快排过程就是寻找这个支点过程

先看普通的快排(普通单路快排)

代码如下

 

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(n2)级别,为什么?

首先快排是个遍历下个数字并确定支点过程,首先先假设第一个就是支点,那么后面的书如果比支点大,说明支点不需要移动(因为右边统一比支点大),如果后面的数比支点小,说明这个数字应该在支点前面,对不对,这个时候支点实质上应该向左移动一位(因为之前那个位置让给比他小的那个数字了,注意这里实质上,因为本轮排序没结束,还没有找到支点应该在的准确位置,所以支点还是第一个),然后将加1后支点所在位的当前数字和当前数交换(因为新的位置已经被支点占据了,而原支点位置是比支点小的数字),依次类推,最后找到所有混乱数组里面,最后一个小于支点的数字,统计出所有小于支点的总数是k,那么这个k就是支点应该在这个混乱数组里的具体位置!然后再依此支点为分界点,递归排序

ok,那么上面代码存在什么问题呢?假设待排序数组(比如[1,2,3,4,5,6,7]),默认取第一个,可是往后面遍历的时候,后面数字全是大于1的,第一轮循环结束,时间复杂度n,再取第二个2,结果发现后面的又是全大于2的,依次循环,不难发现用上述代码是n2的复杂度

我们无法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)的时间复杂度

相比较双路快排是找大于或者等于对应位置,三路快排是说我找的是一个区间,找的是一个等于指针的那个区间

以上是关于快排,随机快排,双路快排,三路快排的理解的主要内容,如果未能解决你的问题,请参考以下文章

快排之三路快排

快排之三路快排

快速排序(part two)

40分钟掌握快速排序-三路快排算法

排序算法:堆排,快排的实现(快排的三种实现方法以及快排的优化)

排序算法:堆排,快排的实现(快排的三种实现方法以及快排的优化)