快排之单边扫描和双边扫描策略

news/2024/5/19 23:36:54 标签: 快速排序, 排序算法

#前言

在这么多的排序算法中,不考虑桶排序,只有堆排序、快速排序、归并排序的时间复杂度是O(nlogn),今天就来讨论一下神奇的排序算法——快速排序

之所以说快排是个神奇的算法,不仅仅是因为其时间复杂度较低,其中包含的几个比较重要的思想非常值得我们学习一下

#关于快速排序

##基本思想

快速排序算法包含其中一个思想就是分治思想,这也是快排的时间复杂度能够保持在O(nlogn)的关键因素

  • 每次从序列中选出一个基准值,其他数依次和基准值作比较,比基准值大的放在右边,比基准值小的放在左边,然后再对左边和右边的数分别选出一个基准值,进行同样的比较移动,重复步骤,直到最后变为单个元素,整个数组就成为了有序的序列

#算法实现

##单边扫描

  • 随意抽取一个数作为基准值,同时设定一个标记 mark 代表左边序列最右侧的下标位置,当然初始为 0 ,接下来遍历数组,如果元素大于基准值,无操作,继续遍历,如果元素小于基准值,先将 mark 所在位置的元素和遍历到的元素交换位置,再把 mark + 1 ,mark 这个位置存储的是比基准值小的数据,当遍历结束后,将基准值与 mark 所在元素交换位置即可
  • mark的作用就是,从左至右遍历数组时,将第一个小于基准值的数放在0的位置,将第二个小于基准值的数放在1的位置,以此类推...
  • 代码:
public class QuickSort1 {
        public static void main(String[] args) {
            nt[] A = {3,2,1,5,4,7,6,6};
            sort(A);
            //partition(a,0,a.length-1);
            System.out.println("排序后的a:"+ Arrays.toString(A));
        }
        private static void sort(int[] arr){
            sort(arr,0,arr.length-1);
        }

        private static void sort(int[] arr,int startIndex,int endIndex){
            if(startIndex >= endIndex) return;
            int p = partition(arr,startIndex,endIndex);
            sort(arr,startIndex,p-1);
            sort(arr,p+1,endIndex);
        }

        private static int partition(int[] arr,int startIndex,int endIndex){
            int mark = 0;
            int pilot = arr[startIndex];
            for(int i=0;i<arr.length;i++){
                if(arr[i]>pilot){
                    continue;
                }
                if(arr[i]<=pilot){
                    int pre = arr[mark];
                    arr[mark] = arr[i];
                    arr[i]=pre;
                    mark++;
                }
            }
            int pre = arr[startIndex];
            arr[startIndex] = arr[mark-1];
            arr[mark-1] = pre;
            return mark-1;
        }
}

##双边扫描

  • 随意抽取一个数作为基准值,然后从数组左右两边进行扫描,先从左往右找到一个大于基准值的元素,将下标指针记录下来,然后转到从右往左扫描,找到一个小于基准值的元素,交换这两个元素的位置,重复步骤,直到左右两个指针相遇,再将基准值与左侧最右边的元素交换
  • 代码:
public class QuickSort2 {
        public static void main(String[] args) {
            int[] A = {5, 3, 6, 4, 1, 7, 9, 2, 8};
            sort(A);
            System.out.println("排序后的A:" + Arrays.toString(A));
        }

        private static void sort(int[] arr) {
            sort(arr, 0, arr.length - 1);
        }

        //用递归
        private static void sort(int[] arr, int startIndex, int endIndex) {
            //终止条件
            if (endIndex <= startIndex) {
                return;
            }
            int pivotIndex = partition(arr, startIndex, endIndex);
            sort(arr, startIndex, pivotIndex - 1);
            sort(arr, pivotIndex + 1, endIndex);
        }

        private static int partition(int[] arr, int startIndex, int endIndex) {
            int left = startIndex;
            int right = endIndex;
            int pivot = arr[startIndex];
            //这里采用直接覆盖的方式,这样可以省去每次申请的temp变量开销
            while (left < right) {
                while (left < right && arr[right] >= pivot) right--;  // ①
                arr[left] = arr[right];
                while (left < right && arr[left] <= pivot) left++;    // ②
                arr[right] = arr[left];
            }
            arr[left] = pivot;
            return left;
        }
}

#单边扫描与双边扫描基准值选取区别

对于单、双边扫描的算法实现是不同的,但是有个细节,那就是选取基准值pivot

##区别

  • 其中单向扫描可以随机地选择pivot值,即可以选择待排序数组中的任意一个元素作为pivot,对结果是没有影响的
  • 而双边扫描是不可以随意选取基准值的,一般情况下我们选择第一个或者最后一个元素,但是注意选取的元素位置应与最初扫描方向相反,即:
    • 当我们选择第一个元素作为pivot时,最初应该先右边向左扫描,也就是上述代码① 应该在代码② 之前
    • 反之,当我们选择最后一个元素作为pivot时,最初应该先右边向左扫描,也就是上述代码① 应该在代码② 之后

##为什么呢?

  • 其实可以这么理解
  • 当最后左右指针即将相遇时(左右指针挨边),此时,左指针指向所有小于基准值的最右边,右指针指向所有大于基准值的最左边
  • 下一步就是指针相遇(指的是同一个位置),如果选择第一个作为pivot,按照上述规则,首轮需要右指针先从右往左扫描,有人会问:如果我偏不这么做,那么接下来将会发生什么?
  • 如果我们让左指针先从左往右扫描,那么此时左指针往右移动一步与右指针重合,但是刚刚说过,此时右指针指向所有大于基准值的最左边
  • 也就是此时重合的位置指向的元素大于基准值,如果此时交换,那么第一个元素就是一个大于基准值的元素
  • 最终我们的排序结果中,第一个元素就是一个大于pivot的元素
  • 然而,如果我们遵循上述规则,则不会发生这种情况

#快排衍生题

快排还有一些衍生题目,这里简单提一下

如何找出一个无序数组的中位数?

  • 思路无非是找到排序之后位于中间的那个数(如果是偶数个,则需要找出中间的两个数)

  • 暴力解

    • 可以先将数组排序为有序数组,再找出中位数
  • 快排思路

    • 对数组进行partition,每次的 partition 返回一个位置,直到找到中间那个位置

找出一个无序数组中第 k 小的元素,即排序后的第 k-1 位置上的元素?

  • 同样,利用快排思想

http://www.niftyadmin.cn/n/1690383.html

相关文章

C++字符读入函数(getchgetchar)

对于字符的读入&#xff0c;我们有scanf和cin这两个最为经典的函数&#xff0c;但是我们发现这两个函数写的都会比较麻烦或难看&#xff0c;而且耗费的时间复杂度较多。 而cstdio和conio.h头文件为了解决这个问题分别提供了两个函数——getch和getchar函数。 getch函数 所在…

什么是卡特兰数

#简介 卡特兰数是组合数学中一个常出现在各种计数问题中的数列。以比利时的数学家欧仁查理卡塔兰 (1814–1894)的名字来命名。 数列的前几项为&#xff1a;1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862&#xff0c;… 卡特兰数Cn满足以下递推关系&#xff1a; Cn1 C0Cn C1Cn-1 …

FontAwesome动态旋转图标类(fa-spinfa-pulse)

不管是我们在网上打游戏&#xff0c;还是上网学习的时候&#xff0c;网页在加载时往往都会出现一个不停在转着圈的加载图标&#xff0c;可是这种图标是怎么实现的呢&#xff1f;其中一种方法就是用FontAwesome图标提供的两个很不错的类——fa-spin和fa-pulse。 fa-spin类 <…

FontAwesome静态旋转翻转图标类(fa-rotatefa-flip)

对于一个FontAwesome图标有时我们希望它能旋转个多少度或者翻转一下来显示&#xff0c;而不是直接显示上去&#xff0c;也因此FontAwesome提供了两个类——fa-rotate和fa-flip。 fa-rotate类 <i class"fa fa-shield fa-rotate-90"></i>功能&#xff1a…

Java集合源码解析之——HashMap

1 前言 该文章的内容是建立在读者对HashMap有初步了解的基础上的 HashMap中有很多知识点&#xff0c;比如哈希表、位运算、链表、红黑树等&#xff0c;HashMap 的源码也是很值得大家去学习的 2 哈希表 在讲源码之前首先了解一下什么是哈希表 Hash表也称为散列表&#xff0c;也有…

C++暴力判断质数

质数是指在大于1的自然数中&#xff0c;除了1和它本身以外不再有其他因数的自然数。 质数的判断在很多题目中都会用到&#xff0c;而这里给出了一种最为常用的判断质数的方法——暴力求法。 暴力判断质数 如果说题目中只是要去判断很少的几个质数&#xff0c;就直接暴力判断…

二分查找模板代码

前言 在刷题的过程中我们会经常用到二分查找算法&#xff0c;该算法听着很简单&#xff0c;当然实现起来也不难。 但是&#xff0c;我们会时常碰到一些特殊情况&#xff0c;根据情况不同&#xff0c;二分查找的代码也会有细微差别&#xff0c;所以真正掌握二分的代码也不是件…

C++推法求质

判断一个质数我们往往直接暴力&#xff0c;可如果是要找出从1到n的所有质数&#xff0c;并将它们存在一个数组里时&#xff0c;我推荐还是使用推法求质吧。 推法求质 思路很简单&#xff0c;就是从2开始枚举到n&#xff0c;每次用已经找到的质数去判断这个数是不是质数&#…