(算法笔记)将单向链表按某值划分成左边小、中间相等、右边大的形式

荷兰国旗问题

问题一

给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)

问题二(荷兰国旗问题,涉及到快排的改进版本的应用)

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度0(1),时间复杂度0(N)

解决方案:可以参考图中的区域,实际的代码在下面的quick_sort2方法中有体现

荷兰国旗问题

快排思想

1、快速排序的基本思想:

  • 快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。

  • 之后分别对这两部分记录递归地进行第一步中的步骤,以达到整个序列有序的目的。

2、快速排序的三个步骤:

(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)

(2)分割操作(两种方法):

  • 以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大(以下代码中的partition方法实现了这一思想);
  • 此外还有一种提升效率的分割操作:以该基准在序列中的实际位置,把序列分成三个子序列。此时,在基准左边的元素都比该基准小,等于基准的元素都放在中间,在基准右边的元素都比基准大(以下代码中的partition2方法实现了这一思想),可以参考以下案例:

3、选择基准的方式

对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。

最理想的方法是,每一次选择的基准恰好能把待排序序列分成两个等长的子序列

代码


public class QuickSort {


    public static void main(String []args) {
        int []arr = {50, 11, 20, 20, 15, 17, 23, 20, 30, 40, 10, 50, 20};

        //quick_sort(arr,0, arr.length - 1);
        quick_sort2(arr,0, arr.length - 1);
		//partition(arr, 0, arr.length - 1);
        for (int i = 0; i < arr.length; i++)
            System.out.print(arr[i] + ", ");
        //quickSort(arr,0, arr.length - 1);

    }

    public static void quickSort(int[] num,int i,int j){
        if(i >= j){//只剩一个元素不用处理直接结束。
            return;
        }
        //选取基准数
        int val =num[i];
        int l = i;
        int r = j;
        while(l < r){//当l == r时,就是调整完成时
            //从后往前找第一个小于val的数字
            while (l < r && num[r] > val){
                r --;
            }
            if(l < r){//找到了数字
                num[l++] = num[r];
            }
            //从前往后找第一个大于val的数字
            while (l < r && num[l] < val){
                l ++;
            }
            if(l < r){//找到了数字
                num[r--] = num[l];
            }
        }
        //l==r,基准数放进去
        num[l] = val;
        quickSort(num,i,l-1);
        quickSort(num,l+1,j);
    }

    public static void quick_sort(int arr[], int low, int high)
    {
        if (arr.length < 2) return;
        // 这个必须要有,否则要
        if(low >= high){//只剩一个元素不用处理直接结束。
            return;
        }

        int l = partition(arr, low, high);
        System.out.println(l);
        quick_sort(arr, low, l - 1);
        quick_sort(arr, l + 1, high);

    }

    public static int partition(int arr[], int low, int high)
    {
        int l = low, r = high;
        int temp = arr[low];
        while(l < r)
        {
            while(l < r && arr[r] >= temp) --r;
            if (l < r)  arr[l++] = arr[r];

            while(l < r && arr[l] <= temp) ++l;
            if (l < r) arr[r--] = arr[l];

            //while(l < r && arr[l])
        }
        arr[l] = temp;
        return l;
    }

    // 快排的升级版本,解决荷兰国旗问题第二个问题
    public static void quick_sort2(int arr[], int low, int high)
    {
        if (arr.length < 2) return;
        // 这个必须要有,否则要
        if(low >= high){//只剩一个元素不用处理直接结束。
            return;
        }

        // 在快排开始前,先随机选中一个元素与最后一个元素交换
        swap(arr, low + (int)(Math.random() * (high - low + 1)), high);

        int p[] = partition2(arr, low, high, arr[high]); // partition函数的返回值是等于区域的左边界和右边界
        //System.out.println(l);
        quick_sort(arr, low, p[0] - 1);  // 小于区域里面
        quick_sort(arr, p[1] + 1, high); // 大于区

    }
     // 该方法的返回值返回的是等于区间的左右边界下标
    public static int[] partition2(int arr[], int low, int high, int target_value)
    {
        // low表示的是当前数的位置, 只使用当前数
        int less = low - 1; // 小于区域的右边界
        int more = high + 1; // 大于区域的左边界

        while(low < more)
        {
            // 如果数组的当前值小于等于目标值,则将该值与小于区域的右边界的下一个数做交换,小于区域右扩,即++less, 当前数跳下一个,即low++
            if (arr[low] < target_value)
                swap(arr, ++less, low++);
                // 如果数组的当前值大于目标值,则将该值与大于区域的左边界的前一个数做交换,大于区域左扩,即more--, 同时当前数的index不变
            else if (arr[low] > target_value)
                swap(arr, low, --more);
                // 如果数组的当前值等于目标值,则当前数跳到下一步,其他什么也不做
            else low++;
        }
        // swap(arr, more, high);
        return new int[]{less + 1, more};
    }
    public static void swap(int arr[], int i, int j)
    {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

算法性能

  1. 时间复杂度

    • 最坏的情况时间复杂度是O(n^2), 此时序列是已经全部有序(只有在此时,每一次的划分,都只产生一个长度不为0的子序列),此时会进行n次划分;
    • 最好的情况是数组中的元素无序,且此时每一次的找出的基准,都能放到数列中的位置,此情况下的时间复杂度是O(n * log(2, n))。
    • 因为基准选择的好坏与否,是一个概率问题,
      经由概率公式的计算,平均的时间复杂度为O(n * log(2, n))
  2. 空间复杂度,

主要是递归造成的栈空间的使用,

- 最好情况,递归树的深度为 logn,其空间复杂度也就为 O(logn),

- 最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n),

- 平均情况,空间复杂度也为O(logn)。
  1. 稳定性

    同简单选择排序:由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。

  2. 还有其他的n*log(2,n)的排序算法,为什么只有快排快,是因为相比的其他类型的nlogn的算法,快排的代码十分简洁,所以最快

  3. 快排的改进版本和原版相比:

    • 改进版本比原版算法的速度稍快,但是这只在序列中有多个相等元素的时候才成立,如果序列中所有的元素都不同,则他们两个的执行一样
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值