[算法] Golang 快速排序

2,810 阅读4分钟

说在前面

作为写排序算法的开山篇,我首先选择了快速排序,作为排序大家族中的一员,快速排序在大多数情况下都有着优秀的综合性能,快速排序的快速也算是实至名归了。

核心思想

快速排序就是通过多次的比较与交换来完成排序, 而这个过程又被分成了多次重复的单趟排序:

  • 首先设定一个分界值(key),通过该分界值将数组分成左右两部分。

  • 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于分界值,而右边部分中各元素都大于或等于分界值。

  • 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。

  • 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。

以下面的数组为例,可以看到,在完成单趟排序后,无论key的左边还是右边是否有序,key都来到了它在整个数组有序时应该待的位置,所以对于这个key来说,它已经被排好序了

image.png

快速排序的三种方法

1.hoare方法

其单趟排序的思路是:取区间中最左或最右边的元素为key,定义两个变量,这里假设是i和j,j从区间的最右边向左走,找到比key小的元素就停下。i从最左边向右走,找到比key大的元素就停下。然后交换i和j所指向的元素,重复上面的过程,直到i,j相遇,此时i,j相遇的位置就是key排序后待的位置。

func QuickSort(arr []int, left, right int) {
   if left >= right {
      return
   }
   i, j := left, right
   privot := arr[i]    //privot就是我们单趟选择的分界值,一般我们是选择左右边界值,不选择中间值
   for i < j {
      //每次找到大于key或者是小于key的值就将 i, j 对应的值进行交换
      for i < j && arr[j] >= privot {
         j--
      }
      arr[i] = arr[j]
      for i < j && arr[i] <= privot {
         i++
      }
      arr[j] = arr[i]
   }
   //当for循环退出时,此时i的位置就是key值在排序后应该在的位置
   arr[i] = privot
   QuickSort(arr, left, i-1)   //递归将key左边的数组进行排序
   QuickSort(arr, i+1, right)  ////递归将key右边的数组进行排序
}

2.挖坑法

这个方法单趟排序的思路是:取最左或最右边的元素为key,假设把这个位置“挖空”,让最右边的q向左走,直到遇到比key小的数,将其放到key的位置,自己的位置变“空”。直到pq相遇,那么这个位置就是最终的坑位,再将key填入这个坑位,就完成了一趟排序。

func QuickSort(arr []int, left, right int) int {
    key := left  //取最左边的元素作为key
    privot := arr[left]
    for left < right {
        for left < right && arr[right] >= privot {  //从右往左找,找到比privot小的就停下来
            right--
        }
        arr[key] = arr[right]   //将元素填入坑内
        key = right    //更新坑的位置
        for left < right && arr[left] <= privot {   //从左往右找,找到比privot大的就停下来
            left++
        }
        arr[key] = arr[left]
        key = left
    }
    arr[key] = privot
    return key  
}

3.快慢指针法

取最左边的数为key,定义两个快慢指针,都从key的下一位往右走,fast每走一步判断一下它指向的元素是否小于key,若小于则交换fast和slow位置的元素,并且让slow向前走,直到fast走到底,结束循环。最后让slow和key位置的值交换。再返回key的位置。

func QuickSort(arr []int, left, right int) int {
    key := left   //取最左边的为key
    fast := key+1 //快指针
    slow := key   //慢指针
    for fast <= right {
        if arr[fast] < arr[key] {   //当快指针指向元素小于key就交换
            arr[slow], arr[fast] = arr[fast], arr[slow]
            slow++
        }
        fast++
    }
    arr[key], arr[slow] = arr[slow], arr[key]   //慢指针回退一位再交换
    return slow   //返回key的位置
}

两种优化快速排序的思想

1.三数取中

面对完全有序的数组,快速排序每趟排序后,key的位置都在边缘,每层递归都只能固定一个数,时间复杂度变成了O(N^2)。

面对这种情况,我们可以在取key时动手脚。每次取key我们不再只取最左或最右的值。而是对比最左、最右、中间的三个元素,取三个元素中,值在另外两者中间的元素作为key。这样,就打乱了有序数组,大大加快了快速排序在面对这种情况时的速度。

2.小区间优化

快速排序对一个元素不多的数组排序,仍需要进行多次递归调用,我们知道递归是比较消耗资源的,所以为了避免在快速排序递归的最后几层大量调用函数,我们可以在数组元素较少时不再递归,而是采用选择排序替代,这样就能在不损失多少速度的情况下减少大量的递归次数,达到优化速度的目的。 (blog.csdn.net/LiangXiay/a…)