快速排序的多种实现写法

快速排序

分析

时间复杂度

  1. 快速排序和Shell排序都有多种策略可选择,快速排序对于中轴值的选择,选择得好每次左右子问题的规模就相近,则只需要划分lgN次就可以将整个排序划分完全
  2. 而每个子问题划分成下一个子问题的时间复杂度是n(即partition()算法).所以整体就是NlgN.
  3. 如果划分得不好,即每次左右子问题规模差距极大,即每次划分为长度为1和left(剩余)两个子问题,则共需划分N次,所以最差时间为N^2

空间复杂度

  1. 由划分子问题的次数决定调用栈的次数,和上面一样是[lgn,n]

稳定性

  1. 稳定性取决于划分子问题时是否保证稳定性.
  2. 快慢指针法是不稳定的,因为也是使用交换法进行调整分组,这种交换是直接交换位于"大于段"的任意两个元素来给"小于等于段"腾位置,所以对于"大于段"中的两个等值元素有可能被打乱顺序.
  3. 对于顺序存储结构应该是不稳定的 (如果能将partition()函数写成稳定性的划分是可以实现稳定的).
  4. 链式存储结构可实现稳定性, patition()函数目的同 LC86. 分隔链表 对于这题我有大多人没有想到的方法, 最后附上

算法实现

快排的partition()函数

  1. 这个算法其实是比较难的, 快排中其它部分的代码几乎就是模板一样固定死了. 而partition()函数的写法有3种及以上.
  2. 等价于下面两个问题 (第一个问题勉强算是, 只是需要稍微改造一下, 快排中用到时我进行了改动)
  • 给定数组nums,再给一个数num,把小于等于num的数放在数组的左边,大于num的放数组右边.空间O(1)时间O(n)
  • 给定数组nums,再给一个数num,把小于num的数放在数组的左边, 等于num的数放在数组的中间,大于num的放数组右边.空间O(1)时间O(n)
public class Partition extends Sort { //Sort抽象类最后补充
    public static void main(String[] args) {
        new Partition().correctTest(null);
    }

    @Override
    public void sortNums(int[] nums) {
//        segmentTwoPart_LeftRightPoint(nums, 6);
//        segmentTwoPart_FastSlowPoint(nums, 1);
        segmentTwoPart_LeftRightPoint_Variant(nums, 6);

    }

    public void segmentTwoPart_FastSlowPoint(int[] nums, int num) {
        /*
        1. 有两个指针,将数组划分成三段:小于等于段/大于段/未筛选段
        2. (-∞,lessEqualR-1]是小于等于段,[lessEqualR,i]是大于段,[i+1,∞)未筛选
        3. i遍历过的地方都是分好段的,方式是:nums[i]属于哪一段就放到哪一段的末尾:
          nums[i] <= num时就要放到小于等于段末尾. 当>时就不动,自动放到大于段末尾
         */
        int lessEqualR = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] <= num) { // 作为快速排序的子问题划分算法,不管这里的"=="是否触发swap(),都是不稳定的.
                // 因为: 只要触发交换,即交换[lessEqualR,i]大于段中nums[lessEqualR]和nums[i],都会导致原本minRight和i位置互换,
                // 而之后这两个元素并会再次被遍历到,所以只有一次交换操作,会导致大于段失去稳定性
                swap(nums, i, lessEqualR);
                lessEqualR++;
            }
        }
        // lessEqualR-1就是小于等于段尾元素下标
        System.out.println(lessEqualR - 1);
    }

    /**
     * 快慢指针的另一种写法,思想是一样的,只是写法稍有不同
     */
    public void segmentTwoPart_FastSlowPoint_Variant(int[] arr) {
        int lessEqualR = -1; // 代表小于段的尾元素下标
        int index = 0;
        int N = arr.length;
        while (index < N) {
            if (arr[index] <= arr[N - 1]) {
                swap(arr, ++lessEqualR, index++);
            } else {
                index++;
            }
        }
        // lessEqualR代表小于等于段的尾元素下标
        System.out.println(lessEqualR);
    }

    /**
     * 也是采用"分段思想",不过采用相向指针
     * <p>
     * 就是将segmentThreePart_OppositePoint()划分"大于段"的操作用在了划分"小于等于段".
     * 1.将"未筛选段"逐个遍历,满足小于等于的条件就放到"小于等于段"的下一个位置,即未筛选段头
     * 2.放的方式是交换,交换后继续筛选原本未筛选段头的元素即遍历的当前位置元素
     * 3.不满足条件就不动,注意因为不动这些不满足条件的,所以要让不满足条件的远离"小于等于段",所以遍历时从"小于等于段"的另一端开始
     */
    public static void segmentTwoPart_OppositePointer(int[] nums, int num) {
        int lessEqualR = 0; //"小于等于段"尾, 也是"已筛选段"的头
        for (int i = nums.length - 1; i >= lessEqualR; i--) {
            if (nums[i] <= num) {
                swap(nums, lessEqualR, i);
                lessEqualR++;
                i++;
            }
        }
        // lessEqualR代表小于等于段的尾元素下标
        System.out.println(lessEqualR);
    }

    /**
     * 过了很久以后我能想起来的解法,是segmentTwoPart()快慢指针演变的
     * 1. 有三个指针,将数组划分成三段:小于段/等于段/大于段/未筛选段
     * 2. (-∞,lessEqualR-1]是小于段,[lessEqualR,equalsR]等于段,[equalsR,i]是大于段,[i+1,∞)未筛选
     * 3. i遍历过的地方都是分好段的,方式是:nums[i]属于哪一段就放到哪一段的末尾,nums[i] < num时就要放到小于段末尾;nums[i] = num时就要放到等于段末尾;当>时就不动,自动放到大于段末尾:
     *    a) nums[i]<num时放到小于段末尾,又因为小于段和等于段紧挨着,所以只能让等于段整体后移才行.方式是先放到等于段尾下一个位置,然后让等于段头和段尾交换,
     *     因为等于端头正好在小于段尾,这样等于段头元素拼接到等于段尾,小于段尾的下一个位置拼接了nums[i]
     *    b) nums[i]=num时放到等于段末尾,就和segmentTwoPart()的快慢指针放置小于等于段的方式一样
     *    c) nums[i]>num时不动,自动放到大于段末尾
     */
    public void segmentThreePart_FastSlowPoint(int[] nums, int num) {
        int lessR = 0; //小于段下一个元素位置,即等于段头
        int equalsR = 0; // 等于段下一个元素位置,即大于段头
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] < num) {
                swap(nums, i, equalsR);
                swap(nums, lessR, equalsR);
                lessR++;
                equalsR++;
            } else if (nums[i] == num) {
                swap(nums, i, equalsR);
                equalsR++;
            }
        }
        System.out.println("等于段的范围: [" + lessR + ", " + (equalsR - 1) + "]");
    }

    /**
     * 1. 小于段就是采用segmentTwoPart_FastSlowPoint()方法划分
     * 2. 遇到"=="则不动,即放在等于段
     * 3. 遇到">"放到大于段,放的方式是拼接到大于段前一个位置,交换时原大于段前一个位置的元素并没有被遍历到,
     * 所以交换完下一次继续遍历该元素现在的位置,即nums[i]
     */
    public void segmentThreePart_OppositePoint(int[] nums, int num) {
        int lessR = 0; // 小于段下一个位置,即等于段头
        int moreL = nums.length - 1; // 大于段前一个位置,即等于段尾
        for (int i = 0; i <= moreL; i++) {
            if (nums[i] < num) {
                swap(nums, lessR, i);
                lessR++;
            } else if (nums[i] == num) {
            } else if (nums[i] > num) {
                swap(nums, moreL, i);
                moreL--;
                i--;
            }
        }
        System.out.println("等于段的范围: [" + lessR + ", " + moreL + "]");
    }

    /**
     * 左右双指针法
     * 重点: 谁先移动,退出循环时谁就会超出其代表的范围
     *
     * 注意:不支持nums.length=1的情况
     */
    public void segmentTwoPart_LeftRightPoint(int[] nums, int num) {
        if (nums.length <= 1) {
            return;
        }
        int left = 0; // 小于等于段段尾
        int right=  nums.length - 1; // 大于段段头
        while(left < right) {
            while (left < right && nums[left] <= num) {
                left++;
            }
            while (left < right && nums[right] > num) {
                right--;
            }
            if (left < right) { //需要进行交换
                swap(nums, left, right);
            }
        }
        // 最终肯定是left主动越界到right位置,变成小于等于段段尾下一个位置
        System.out.println(left - 1);
    }

    /**
     * 左右双指针法
     * 先让right移动, 谁先移动,退出循环时谁就会超出其代表的范围,这里right超出范围
     */
    public void segmentTwoPart_LeftRightPoint_Variant(int[] nums, int num) {
        if (nums.length <= 1) {
            return;
        }
        int left = 0; //小于等于段段尾
        int right=  nums.length - 1; // 大于段段头
        while(left < right) {
            while (left < right && nums[right] > num) {
                right--;
            }
            while (left < right && nums[left] <= num) {
                left++;
            }
            if (left < right) {
                swap(nums, left, right);
            }
        }
        // 最终肯定是right主动越界left到位置,所以left是一直处于小于等于段段尾
        System.out.println(left);
    }

    /**
     * 坑法 - 同数据结构与算法C语言版
     * 局限性: 划分两段的依据值pivot必须是头元素, 因为left要在第一个坑处.
     * 不过不影响快排的pivot策略,因为只需要在找到最合适的pivot后将头元素和该pivot交换位置即可
     *
     * 分析: 根据谁先移动,最终谁就会越界,所以right会跑到大于段前一个位置,此时left和right重叠为坑,填坑后left还表示小于等于段段尾
     */
    public void segmentTwoPart_Pit(int[] nums) {
        int left = 0; //小于等于段段尾
        int right = nums.length - 1; //大于段段头
        int pivot = nums[0]; //划分两段的依据值
        while (left < right) {
            while (left < right && nums[right] > pivot) {
                right--;
            }
            nums[left] = nums[right];
            while (left < right && nums[left] <= pivot) {
                left++;
            }
            nums[right] = nums[left];
        }
        int pivotIndex = left;
        nums[pivotIndex] = pivot;
        System.out.println("小于等于段尾元素下标: " + pivotIndex);
    }
}

快速排序完整代码

public class QuickSort extends Sort {
    public static void main(String[] args) {
        new QuickSort().correctTest(null);
    }

    @Override
    public void sortNums(int[] nums) {
        quickSortProcess(nums);
    }

    public void quickSortProcess(int[] nums) {
        quickSortSegmentTwoPart01(nums, 0, nums.length - 1);
    }

    /**
     * 选择pivot的策略
     * 1.一定不能始终选择begin作为pivot,因为这样对sorted已排序(降序/升序)的数组效率很低
     * 2.还有一种三值取中法,暂时不研究
     */
    public int quickSortPivotChoose(int[] nums, int begin, int end) {
        // 暂定为随机选择元素作为pivot
        int index = begin + (int)(Math.random() * (end - begin + 1));
        // 将随机的pivot放到begin位置,可以适用于坑法/segmentTwoPart_FastSlowPoint()的partition()算法
        swap(nums, begin, index);
        return begin;
    }

    public void quickSortSegmentTwoPart01(int[] nums, int begin, int end) {
        if (begin >= end) {
            return;
        }

        int pivotChose = quickSortPivotChoose(nums, begin, end);
        int[] pivotIndexes = segmentThreePart_FastSlowPoint(nums, begin, end, nums[pivotChose]);

        // 递归时,必须要让左和右区间都变化1个距离,因为pivotIndex可能每次都不变,导致递归过程无法减少子问题的长度造成stackOverFlow
        // 对比归并排序中mergeSort()中mid=(begin+end)/2,则mid会不断靠近begin,而这里pivotIndex无法确定其变化
        quickSortSegmentTwoPart02(nums, begin, pivotIndexes[0] - 1);
        quickSortSegmentTwoPart02(nums, pivotIndexes[1] + 1, end);
    }

    /**
     * 划分为3个区间,快慢指针做法
     */
    public int[] segmentThreePart_FastSlowPoint(int[] nums, int begin, int end, int pivot) {
        int lessR = begin;
        int equalR = begin;
        for (int i = begin; i <= end; i++) {
            if (nums[i] < pivot) {
                swap(nums, i, equalR);
                swap(nums, lessR, equalR);
                equalR++;
                lessR++;
            } else if (nums[i] == pivot) {
                swap(nums, i, equalR);
                equalR++;
            }
        }
        int equalBegin = lessR;
        int equalEnd = equalR - 1;
        int[] pivotIndexes = new int[]{equalBegin, equalEnd};
        return pivotIndexes;
    }


    /**
     * 测试其它partition()函数写法
     * 快排的递归过程只有一种写法,写法比较多的是partition()算法
     */
    public void quickSortSegmentTwoPart02(int[] nums, int begin, int end) {
        if (begin >= end) {
            return;
        }

        int pivotChose = quickSortPivotChoose(nums, begin, end);
        int pivotIndex = segmentTwoPart_FastSlowPoint(nums, begin, end, nums[pivotChose]);

        quickSortSegmentTwoPart01(nums, begin, pivotIndex - 1);
        quickSortSegmentTwoPart01(nums, pivotIndex + 1, end);
    }

    /**
     * 划分为2个区间(但是要保证左区间最后一个元素一定是pivot),快慢指针做法
     */
    public int segmentTwoPart_FastSlowPoint(int[] nums, int begin, int end, int pivot) {

        int lessEqualR = begin;
        for (int i = begin; i <= end; i++) {
            if (nums[i] <= pivot) {
                swap(nums, i, lessEqualR);
                lessEqualR++;
            }
        }
        // lessEqualR-1就是小于等于段尾元素下标

        // 交换pivot(首元素)和小于等于段尾元素,而不是将数组简单分成两个部分
        swap(nums, begin, lessEqualR - 1);
        return lessEqualR - 1;
    }
}

补充说明

  1. Sort类在排序专栏文章中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值