js数据结构与算法_09_快速排序

 一、实现快排排序

20世纪十大算法之一的快速排序,在大部分情况下都是好于其他排序算法的;

维基百科上对快排的描述:

快速排序(英语:Quicksort),又称分区交换排序(partition-exchange sort),简称快排,一种排序算法,最早由东尼.霍尔提出。在平均状况下,排序n个项目要O(nlogn)次比较。在最坏状况下则需要O(n^2)次比较,但这种状况并不常见。事实上,快速排序O(nlogn)通常明显比其他算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地达成。

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为较小和较大的2个子序列,然后递归地排序两个子序列。

步骤为:

  1. 挑选基准值:从数列中挑出一个元素,称为“基准”(pivot),
  2. 分割:重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(与基准值相等的数可以到任何一边)。在这个分割结束之后,对基准值的排序就已经完成,
  3. 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

递归到最底部的判断条件是数列的大小是零或一,此时该数列显然已经有序。

看完后是不是一头雾水,啥是分治法呀?基准又是啥?是不是看到递归就头疼?

不急不急,我们慢慢来;

“分治法”是分而治之的意思,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并,多依赖于递归。

我们来想想,假如可以让一个元素经过一次逻辑处理后,一次性放到属性这个元素的正确位置,而不是向冒泡那样移来移去呢?

是可以的,我们在一个数组中随便找个值,将比它小的放到它左边,比它大的放到它右边,那么这个值现在的位置一定是正确的!而这个值就是基准值,也就是pivot

我们再把基准值左边已及右边的序列再次重复这种做法,找到基准值,经过排序将基准值一次性安到正确的位置;

而这种方法就是分治法,也就是分而治之,也是快排的核心思想!

二、具体实现

我写算法,一般先写出框架:

function swap(array, m, n) {
  let temp = array[m];
  array[m] = array[n];
  array[n] = temp;
}
function getPivot(left,right) {}
function quickSort(array) {
    let pivot = getPivot(0, array.length)

    function recursion_quickSort(array,left,right) {
        
    }

    return array
}

let arr = [66, 46, 28, 44, 71, 96];
console.log(quickSort(arr));

为了更好理解快排,我尽量把一些处理逻辑抽取出来,如交换位置的 swap 函数、获取基准值的 getPivot、以及快速排序函数;

先把获取基准值的函数写出来,基准值是传入序列的头部、尾部、中间值中的中位数,中间值的索引由下面代码得出:

 let center = Math.floor((left + right) / 2);

然后对这三个值进行排序,从小到大:

 // 排序
  if (array[left] > array[center]) {
    swap(array, left, center);
  }
  if (array[left] > array[right]) {
    swap(array, left, right);
  }
  if (array[center] > array[right]) {
    swap(array, center, right);
  }

然后将center和right-1交换位置,相当于,基准值和倒数第二个元素交换位置;

为什么呢?这样方便等下筛选元素,我们也知道,在数组中间插入和删除比较耗费性能,那怎样筛选出比基准值大以及比基准值小的元素呢?

这个谜题后面揭晓,我们先完成getPivot函数的封装:

function getPivot(array, left, right) {
  let center = Math.floor((left + right) / 2);

  // 排序
  if (array[left] > array[center]) {
    swap(array, left, center);
  }
  if (array[left] > array[right]) {
    swap(array, left, right);
  }
  if (array[center] > array[right]) {
    swap(array, center, right);
  }

  swap(array, center, right - 1);

  return array[right - 1];
}

现在揭晓谜题,如何筛选比基准值大以及比基准值小的元素:

  1. 先把基准值所在的位置和序列最后一个元素交换位置;
  2. 指针bigIndex从序列左边开始寻找比基准值大的元素,找到停下不动换另一个指针smallIndex从序列倒数第二个位置开始寻找比基准值小的元素,找到后停止;
  3. 如果指针bigIndex<smallIndex,那就交换两指针指向元素的位置,并继续移动指针,直到指针bigIndex>=smallIndex,也就是比基准值大的都以找到了,并处理了,此时我们进入下一步处理;
  4. 基准值和bigIndex指向的元素交换(bigIndex在结束循环时一定是大于pivot的数字,且在序列长度大于3的情况下交换后一定是pivot正确的位置)
  5. 按照上面的步骤递归处理,基准值左边以及右边的序列,最终完成快速排序;

为什么4说,一定是基准值正确的位置呢,因为经过3的处理bigIndex前面的元素必定小于等于基准值,而bingIndex后面的元素必定大于等于基准,而bigIndex指向的是大于基准值的元素,一于基准值交换,不久左边全小于,右边全大于吗?这也解释了为什么我们在获取基准值时,把它与right-1进行了位置交换,提升效率嘛;

至于4中为什么说要序列长度大于3,因为在经过getPivot函数的处理后,序列长度为2或3的序列本身就排序好了,交换人家干嘛;

是不是感觉有种恍然大悟的感觉,现在我们来完成所有函数的封装吧!

function swap(array, m, n) {
  let temp = array[m];
  array[m] = array[n];
  array[n] = temp;
}
function getPivot(array, left, right) {
  let center = Math.floor((left + right) / 2);

  // 排序
  if (array[left] > array[center]) {
    swap(array, left, center);
  }
  if (array[left] > array[right]) {
    swap(array, left, right);
  }
  if (array[center] > array[right]) {
    swap(array, center, right);
  }

  swap(array, center, right - 1);

  return array[right - 1];
}
function quickSort(array) {
  function recursion_quickSort(array, left, right) {
    // 结束条件
    if (left >= right) return;
    // 获取枢纽
    let pivot = getPivot(array, left, right);

    // 记录开始位置
    let bigIndex = left;
    let smallIndex = right - 1;

    // 循环查找位置
    while (true) {
      while (array[++bigIndex] < pivot) {}
      while (array[--smallIndex] > pivot) {}
      if (bigIndex < smallIndex) {
        // 表示没重叠,交换两指针指向的元素
        swap(array, bigIndex, smallIndex);
      } else {
        break;
      }
    }

    if (bigIndex != right) swap(array, bigIndex, right - 1);
    // 递归——分而治之
    recursion_quickSort(array, left, bigIndex - 1);
    recursion_quickSort(array, bigIndex + 1, right);
  }

  recursion_quickSort(array, 0, array.length - 1);
  return array;
}

我看看,要额外解释的代码有:

 while (array[++bigIndex] < pivot) {}
 while (array[--smallIndex] > pivot) {}

这个是学coderwhy老师的,第一个bigIndex寻找比pivot大的值,找到退出while循环,第二个找比pivot小的值,找到后退出循环;至于为什么在循环条件那写++,而不是在循环体里面写,当然是为了装杯呀;

if (bigIndex != right) swap(array, bigIndex, right - 1);

这个就是步骤4中的先决条件,序列长度要大于3,而当序列等于3或2时(序列长度为1压根进不了递归函数)bigIndex是等于right的,所以加个判断;

// 递归——分而治之
    recursion_quickSort(array, left, bigIndex - 1);
    recursion_quickSort(array, bigIndex + 1, right);

这两行代码是”分而至之“的体现,将大问题分为小问题,通过递归一层一层的解决小问题,到最后回溯时,大问题就迎刃而解了;

来看个动态,看一下快排的美吧(图来自维基百科)

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值