一文搞懂 三路快速排序,稳定版本快排,非递归版本快排

https://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html

这篇文章说一下快速排序,3路快速排序,稳定快速排序,以及非递归版本的快速排序怎么写。

不懂快速排序的朋友可以点开看一下上面那一个链接,里面有介绍快速排序的概念的。

首先我们来看一下普通快速排序:

普通快速排序:

话不多说先上代码:

public static void main(String[] args) {
        int[] nums = {6,0,3,1,2,4,4,6,7,8,};
//        for (int i = 0; i < nums.length; i++) {
//            nums[i] = (int)(Math.random() * 10);
//        }
        quickSort(nums, 0, nums.length - 1);
        for (int num : nums) {
            System.out.print(num+",");
        }
    }

    //快速排序
    public static void quickSort(int[] array, int left, int right) {
        if (left < right) {
            // mid的值是在 partition之后返回的已经安置好位置的那个元素的位置,这个值接下来被用于作为下面两个递归的左边界和右边界
            int mid = partition(array, left, right);
            //递归排序mid左边那部分
            quickSort(array, left, mid - 1);
            //递归排序mid右边那部分
            quickSort(array,mid + 1, right);
        }
    }

    public static int partition(int[] array, int left, int right) {
        //以左边第一个值作为基准值
        int pivot = array[left];
        int l = left;
        int r = right;
        //这个循环的作用是 让从array[lef] 到 array[right] 间到元素 左边部分的元素小于 pivot 右边部分的元素大于pivot
        while(l < r){
            //找到右边第一个小于 pivot的元素
            while (l < r && array[r] >= pivot){
                r--;
            }
            //找到左边第一个小于pivot的元素
            while (l < r && array[l] <= pivot) {
                l++;
            }
            //交换这两个元素
            swap(array, l, r);
        }
//        array[lef] 到 array[right] 的元素处理完毕后,让pivot元素到它该有到位置,交换pivot和上一个循环截止到那个元素
        // 注意这里 被交换的元素 array[l] 和 array[r]是同一个元素 l和r已经相等了
        //能够交换 只因为 array[l]一定小于等于pivot 这也是为什么上一个大循环中 里面的第一个while循环是 从r开始判断,如果从l开始判断那么最后
        //中止的值是大于等于pivot的

        array[left] = array[l]; // array[left] = array[r]也可以 因为 l和r已经相等了
        array[l] = pivot;
        return l;
    }
    //交换元素
    public static void swap(int[] array, int l, int r) {
        int temp = array[l];
        array[l] = array[r];
        array[r] = temp;
    }

上面那段代码就是普通快速排序, 每次从将要partition的数组区域里面选最左的元素为pivot(基准值) 然后对这段数组区域进行 partition,partition之后的这段数组区域,左边部分小于pivot,右边部分大于pivot,同时pivot 也找到了排序后终身要要放置的位置 并且被放置到了这个位置(终身指的意思是partition之后pivot元素在整个quicksort变得整体有序之后一辈子要放的位置,所以,一次partion就可以 确定一个元素的位置) 注意⚠️:上面这段代码我用最左边的元素当作pivot 当然也可以拿最右边的元素当作pivot,同时也能拿任意一个元素当作pivot, 但是拿最左边和最有边元素当作pivot的话代码边界条件判断比较少,读者可以去网上找一找拿任意元素当pivot的代码,普通快速排序中用最左最右元素当作pivot边界判断少。 但是注意⚠️: 下面我介绍的3路快速排序中:拿任意一个位置的元素当pivot, 代码边界的判断都是一样的。

三路快速排序:

三路快速排序和普通快速排序的主要区别是 在partiontion阶段的不同。

一段数组里面可能出现重复元素,普快的partition在上文中提到了每次只能找到一个pivot的精准定位,重复元素也要再判断一次,但是三路快速排序的partition优化了,它不需要再判断重复元素,在一次partition中返回pivot元素在这段数组区域的最左和最右临界下标. 举个例子: 4为pivot,数组 array[1,2,3,4,5,6,8,4,4,6] 在一次partition后数组会变成下面这个样子

array[3,2,1,4,4,4,5,8,6]

返回的就是 3 和5. array[3]前面的元素比4都小, array[5]后面的元素比4 大

在3路快排的一次partion后 比如说partition返回了[3, 5] 那么再对(left, 3- 1)和(5 + 1, right)进行递归排序, 具体 partion的算法内容 和荷兰国旗问题的方法一摸一样,可参考

漫画:常考的荷兰国旗问题你还不会吗?(初级) - 云+社区 - 腾讯云

不多说上代码:

//三指针 l指向left - 1意义为 小于pivot的数组段段右边界, r初始化为right+1 意义为大于pivot段数组段段左边界
    //一次遍历得到l 和 r 段最终值, l 和 r之间段元素 为值等于pivot 段元素
    public static int[] partitionHeLanGuoQi(int[] array, int left, int right) {
        int pivot = array[left];
        int l = left - 1;
        int r = right + 1;
        int cur = left;
        while (cur < r) {
            if (array[cur] < pivot) {
                swap(array, ++l, cur++);

            } else if (array[cur] > pivot) {
                swap(array, --r, cur);
            } else {
                cur++;
            }
        }
        return new int[]{l, r};
    }

    public static void quickSort3(int[] array, int left, int right) {
        if (left < right) {
            int[] partitionResult = partitionHeLanGuoQi(array, left, right);
            quickSort(array, left, partitionResult[0]);
            quickSort(array, partitionResult[1],right);
        }
    }

稳定快速排序:

不多说先上代码:

public static int partitionStable(int[] array, int left, int right) {
        int[] temp = new int[right - left + 1];
        //制定最左元素为pivot
        int pivot = array[left];
        int idxForTemp = 0; // 初始化指向临时数组temp 的第一个位置
        //先在 left 到right 这个范围内找小于 pivot 到元素
        for (int i = left + 1; i <= right; i++) {
            if (array[i] < pivot) {
                temp[idxForTemp++] = array[i];
            }
        }
        temp[idxForTemp++] = pivot; // 把 pivot放到它该有到位置上面
        int awsFromTemp = idxForTemp - 1; // 保存放pivot到位置 后面要用
        //再遍历一遍数组找到所有大于等于pivot到元素 依次放到临时数组temp中
        for (int i = left + 1; i <= right; i++) {
            if (array[i] >= pivot) {
                temp[idxForTemp++] = array[i];
            }
        }
        idxForTemp = 0;
        int aws = 0;
        //把排好序到部分拷贝回原数组
        for (int i = left; i <= right; i++) {
            if(idxForTemp == awsFromTemp){
                aws = i;
            }
            array[i] = temp[idxForTemp++];
        }
        return aws;
    }
 public static void sort(int[] nums, int left, int right) {
        if (left < right) {
             int i = partitionStable(nums, left, right);
            sort(nums, left, i - 1);
            sort(nums, i + 1, right);
        }
    }

快速排序一般来讲是不稳定的,这是因为每一次partition 是在一个数组本身上通过左右元素交换操作而达到前半部分小,后半部分元素大,而在普通快速排序中 partition里大循环里面的两个小循环条件包含等于pivot的时候停下,所以当等于pivot的时候再进行交换,就会造成不稳定。

解决办法就是:在一次partition中,创建一个临时数组,在要partition的数组里找到小于pivot的元素添加到临时数组中,然后将 pivot添加到临时数组中, 然后再扫描一遍数组将大于等于pivot到元素添加到临时数组中,最后再将临时数组拷贝回原数组,这样partition之后 到快速排序就是稳定的。

非递版归快速排序:

先不说 上代码:

//利用栈先进后出的特性存入 即将要partition的边界条件 即 left 和right 模拟递归进行快排
    public static void quickSortWithoutRegression(int[] nums) {
        //创建一个数组 保存每次要进行partition时候的left和right
        Stack<int[]> stack = new Stack<>();
        stack.push(new int[]{0, nums.length - 1});
        // 开始进行排序
        while (!stack.isEmpty()) {
            //出栈 即将新出的left 和right 搞出来
            int[] pop = stack.pop();
            int left = pop[0];
            int right = pop[1];
            int[] tpReturned;
            if (left < right) {
                tpReturned = partitionInNORegression(nums, left, right);
                stack.push(new int[]{left, tpReturned[0]});
                stack.push(new int[]{tpReturned[1], right});
            }
        }
    }

    //partition可以用3路快排的partition 也可以用普通的partition,也可以用稳定版本的partition,这里用3路版本的partition
    public static int[] partitionInNORegression(int array[], int left, int right) {
        int  l = left - 1;
        int  r = right + 1;
        int cur = left;
        int pivot = array[left];
        while (cur < r) {
            if (array[cur] < pivot) {
                swap(array,++l,cur++);
            } else if (array[cur] > pivot) {
                swap(array, --r,cur);
            }else {
                cur++;
            }
        }
        return new int[]{l, r};
    }

非递归版本的快速排序就是用栈保存partition时候的边界变量,然后每次partition结束后返回的值再和left,right 入栈作为下一次partition的边界变量(即left 和right)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值