快速排序问题探讨

6 篇文章 0 订阅

什么是快速排序?

在一个无序的数组中选择一个基数,将数组中小于等于基数的元素放到基数左边,将大于等于基数的元素放到基数右边,不断循环此操作,完成排序。

如何实现快速排序呢?

指针法

排序原理

找到一个基数,左右设置两个指针,右指针找到小于等于基数的元素,左指针找到大于等于基数的元素,两个元素交换位置。循环上述流程,完成排序。
下面是大神博客中指针法的核心代码:

private static int partition(int[] arr, int startIndex, int endIndex) {
// 取第一个位置的元素作为基准元素
int pivot = arr[startIndex];
int left = startIndex;
int right = endIndex;
while( left != right) {
//控制right指针比较并左移
while(left<right && arr[right] > pivot){
right--;
}
//控制right指针比较并右移
while( left<right && arr[left] <= pivot) {
left++;
}
//交换left和right指向的元素
if(left<right) {
int p = arr[left];
arr[left] = arr[right];
arr[right] = p;
}
}
//pivot和指针重合点交换
int p = arr[left];
arr[left] = arr[startIndex];
arr[startIndex] = p;
return left;
}

问题

上述代码本身没有问题,但是却能引发一些疑惑:

  • 为什么选择第一个元素作为基数之后,要先移动右指针?先移动左指针行不行?能不能把最后一个元素作为基数?
  • 为什么右指针移动时候判断条件是 arr[right] > pivot,左指针移动判断条件是 arr[left] <= pivot ?使用 arr[left] <= pivot 行不行?
  • 根据大神博客的思路和算法,可以看出左右指针最终会重合,重合之后要将当前位置的元素和基数交换,这里的交换就牵扯到一个问题:指针重合位置的元素和基数相比谁更大?

思考

如果我们在判断指针移动的条件是 arr[right] >= pivot,arr[left] <= pivot,那么基数在交换过程中位置是没有改变的,就是说基数是没有参与交换。
基于上述条件我们找到四种场景:1、左端点作为基数,先移动右指针;2、左端点为基数,先移动左指针;3、右端点为基数,先移动左指针;4、右端点为基数,先移动右指针。
左指针找到的元素,我们先定义为“交换前左元素”,那么:交换前左元素 >=基数;右指针找到的元素,我们先定义为“交换前右元素”,那么:交换前右元素 <= 基数(元素交换前);
左右指针元素交换后,左指针指向的元素定义为“交换后左元素”,那么:交换后左元素 <=基数;右指针指向的元素定义为“交换后右元素”,那么:交换后右元素 >=基数;
很明显在同一轮循环中:交换前左元素 = 交换后右元素,交换前右元素 = 交换后左元素

1.左端点作为基数,先移动右指针

这种情况下,右指针先找到一个“交换前右元素”(<=基数),等待左指针找到一个“交换前左元素”(>=基数)来交换。

  • 场景一:右指针找到了“交换前右元素”,左指针找到了“交换前左元素”
    左右元素交换,正常。
  • 场景二:右指针没找到“交换前右元素”,右指针与左指针重合
    此时左指针右两种情况:1、左指针处于初始位置,重合位置元素 = 基数;2、左指针处于交换后的位置,交换后左元素 <= 基数,重合位置元素 <= 基数。
    两种情况下重合位置元素都不大于基数,而基数此时位于最左端,数据交换后仍能保证基数左边数据均小于等于基数,正常。
  • 场景三:右指针找到了“交换前右元素”,左指针没找到“交换前左元素”,左指针与右指针重合
    重合位置为右指针,因为 交换前右元素 <= 基数 ,而基数此时位于最左端,数据交换后仍能保证基数左边数据均小于等于基数,正常。
2.左端点作为基数,先移动左指针

还是相同的场景

  • 场景一:左指针找到了“交换前左元素”,右指针找到了“交换前右元素”
    左右元素交换,正常。
  • 场景二:左指针没找到“交换前左元素”,左指针与右指针重合
    重合位置为右指针,右指针有两种情况:1、处于初始位置,因为右指针还没开始比较,所以初始位置元素是可能大于基数(例如[7,6,5,4,3,8])2、处于交换后的位置,交换后右元素 >=基数。
    此时基数位于最左端,而重合位置元素是可能大于基数的,交换后,基数左边会存在大于基数的元素!失败!
  • 场景三:左指针找到了“交换前左元素”,右指针没找到“交换前右元素”,右指针与左指针重合
    此时指针重合位置 为左指针,交换前左元素 >=基数,基数当前在最左边,交换后情况和场景二相同,失败!
3.右端点作为基数,先移动左指针
  • 场景一:左指针找到了“交换前左元素”,右指针找到了“交换前右元素”
    正常交换,进入下一个循环。
  • 场景二:左指针没找到“交换前左元素”,左指针与右指针重合
    重合位置为右指针,右指针有两种情况:1、处于初始位置,初始位置 = 基数,没有问题 2、处于交换后的位置,交换后右元素 >=基数。
    因为基数在最右边,将重合位置元素和基数交换后,基数右边仍然大于等于基数,正常!
  • 场景三:左指针找到了“交换前左元素”,右指针没找到“交换前右元素”,右指针与左指针重合
    此时指针重合位置 为左指针,交换前左元素 >=基数,与基数交换后,正常。
4.右端点作为基数,先移动右指针
  • 场景一:右指针找到了“交换前右元素”,左指针找到了“交换前左元素”
    正常交换,继续循环。
  • 场景二:右指针没找到“交换前右元素”,右指针与左指针重合
    指针重合位置为左指针,此时左指针右两种情况:1、左指针处于初始位置,因为右指针还没开始比较,所以初始位置元素是可能小于基数(例如[2,9,8,7,6,5])2、左指针处于交换后的位置,交换后左元素 <= 基数。
    此时基数位于最右端,如果和指针重合位置交换元素,那么基数右边存在小于基数的元素,失败!
  • 场景三:右指针找到了“交换前右元素”,左指针没找到“交换前左元素”,左指针与右指针重合
    重合位置为右指针,因为 交换前右元素 <= 基数 ,而基数此时位于最右端,数据交换后和上述场景相同,失败!

总结

如果我们在判断指针移动的条件是 arr[right] >= pivot,arr[left] <= pivot,那么会造成基数在交换过程中位置是没有改变,这种情况下左右指针移动顺序是要注意的:当选择左端点为基数,就先移动右指针;当选右端点为基数,就先移动左指针。
如果随机选择一个位置作为基数,那不管先移动哪边,应该都有可能出问题。

解决办法

判断指针移动的条件写为 arr[right] > pivot,arr[left] < pivot。
小小的改动,在循环时候就可以使基数本身参与交换,这种写法最终是不需要将指针重合位置和基数交换的,反而更加简洁。

实现代码
 //快速排序
    public static void mySort(int[] a, int start, int end) {

        if (start >= end) {
            return;
        }
        //选择第一个元素作为基数
        int base = a[start];
        int left = start;
        int right = end;

        //left != right时交换左右元素
        while (left != right) {

            if(a[left] == a[right]){
                left++;
            }

            //左边向右移动
            while (left < right && a[left] < base) {
                left++;
            }
            //右边向左移动
            while (left < right && a[right] > base) {
                right--;
            }

            //左右元素互换
            int temp = a[left];
            a[left] = a[right];
            a[right] = temp;
        }

       //对基数左右部分进行递归调用
        mySort(a, start, left - 1);
        mySort(a, left + 1, end);
    }
 public static void main(String[] args) {

        int[] a = {4,3,2,1,5,6,7};
        mySort(a, 0, a.length - 1);
        System.out.println(Arrays.toString(a));
    }

填坑法

排序原理

填坑法是先找到一个数作为基数,基数位置作为一个坑,通过指针移动找到合适的值放到坑里,这个指针位置成为新的坑。
例如,将左端点作为基数,此时左端点就是第一个坑,移动右指针,右指针找到一个小于等于基数的元素,将元素复制到基数的位置(最左端),而右指针指向的位置变为第二个坑,等待新元素填充。这时左指针移动,寻找大于等于基数的元素,将元素赋值到坑里(右指针当前位置),左指针作为新的坑。不断循环,知道左右指针重合,将基数填到重合的位置。

问题

  1. 什么位置是坑位?
  2. 什么情况下会改变坑位?

思考

  1. 不移动的指针指向的是坑位
  2. 发生“填坑”操作会导致坑位改变
    那又涉及到基数选择和指针移动顺序的问题了!
    如果选择左端点作为基数,那左指针现在就是坑位了,那它是坑位了,就应该先移动右指针去找元素来“填坑”!
    如果选择右端点作为基数,那就只能先移动左指针了。
    但是如果左端点作为基数,先移动左指针了,或者右端点作为基数,先移动右指针了,会发生什么呢?
选一边端点作为基数,同时先移动同一边指针
1.判断指针移动的条件为 arr[right] > pivot,arr[left] < pivot

这种情况下左指针会先自己填自己一次,其实是没有必要的:
代码:

    public static void mySort(int[] a, int start, int end) {
        int left = start;
        int base = a[start];
        int right = end;
        int index = start;

        while (left < right) {
            if (a[left] == a[right]) {
                right--;
            }

            //左边移动
            while (right > left) {
                if (a[left] < base) {
                    left++;
                } else {
                    //填坑
                    a[index] = a[left];
                    index = left;
                    System.out.println(Arrays.toString(a));
                    break;
                }
            }

            //右边移动
            while (right > left) {
                if (a[right] > base) {
                    right--;
                } else {
                    //填坑
                    a[index] = a[right];
                    index = right;
                    System.out.println(Arrays.toString(a));
                    break;
                }
            }
        }
        //填入基数
        a[index] = base;
        System.out.println(Arrays.toString(a));
        //进入递归
        if (index - 1 > start) {
            mySort(a, start, index - 1);
        }
        if (end > index + 1) {
            mySort(a, index + 1, end);
        }
    }
   public static void main(String[] args) {
       // int[] a = {7, 8, 5, 4, 4, 2, 3, 6, 9, 0};
        int []a = {7,6,5,4,4,8};
        mySort(a, 0, a.length - 1);
        System.out.println(Arrays.toString(a));
    }

运行结果:自己先填充自己一次,填充后和排序前没有任何改变
在这里插入图片描述

2.判断指针移动的条件为 arr[right] >= pivot,arr[left] <= pivot

会造成指针重合位置不是坑位,例如[7,6,5,4,3,8],左指针会一路跑到8的位置,很明显8的位置根本就不是坑位。

选择一边为基点,另一边指针先移动

代码:

    public static void mySort(int[] a, int start, int end) {
        int left = start;
        int base = a[start];
        int right = end;
        int index = start;

        while (left < right) {
            if (a[left] == a[right]) {
                left++;
            }
            //右边移动
            while (right > left) {
                if (a[right] >= base) {
                    right--;
                } else {
                    //填坑
                    a[index] = a[right];
                    index = right;
                    System.out.println(Arrays.toString(a));
                    break;
                }
            }
            //左边移动
            while (right > left) {
                if (a[left] <= base) {
                    left++;
                } else {
                    //填坑
                    a[index] = a[left];
                    index = left;
                    System.out.println(Arrays.toString(a));
                    break;
                }
            }

        }

        //填入基数
        a[index] = base;
        System.out.println(Arrays.toString(a));
        //进入递归
        if (index - 1 > start) {
            mySort(a, start, index - 1);
        }
        if (end > index + 1) {
            mySort(a, index + 1, end);
        }
    }

目前没发现问题。

总结

填坑法也建议:选择左端点作为基点,先移动右指针;选择右端点为基点,就先移动左指针。

全文总结

不论是指针法还是填坑法,都建议选择一个端点作为基点,然后先移动另一边的指针。而判断指针移动的条件为 arr[right] > pivot,arr[left] < pivot。
如果推理过程有错,请留言,我及时改正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值