快速排序计算第K大的数

        先说结论,最终版本代码如下:

public class KthNum {
    public static int k = 2;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (bigK ? arr[arrIndex] > pivot : arr[arrIndex] < pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

      还是老规矩,我会重点谈思路,到底是如何想出这段代码的,请往下看。

       熟悉快排的同学都知道,若不熟悉的同学,可以先看我的这篇白话解析快速排序。快排使用某一个数作为基准点进行分区,通常基准点左边的数都是小于基准点的,在基准点右边的数都是大于基准点的。

        例如1,3,4,2这组数字,使用快排以2为基准点进行倒序排序第一次的结果为3,4,2,1 ,如果你恰好是求第3大的数,那么就是基准点2,只用了一次排序,时间复杂度为O(n)。如果你是求第1大的数或者第2大的数,那么继续在基准点左边进行排序比较即可;如果你是求第4大的数,那么在基准点右边进行排序比较即可。细心的同学可以发现求第K大的数,K的值是和基准点的下标有关系的。第一次排序完成后,基准点2的下标为2,使用下标+1K进行比较,若K等于下标+1直接返回当前下标的值;若K大于下标+1,则继续在基准点右边进行查找;若K小于下标+1,则继续在基准点左边进行查找。循环查找,直到K等于下标+1,则结束。

       基于以上的思路和对快排的理解,我们得出了计算第K大数的第一个版本1.0。

 1.0

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
//        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if (k < p + 1) {
                p = partition(arr, left, p - 1);
            } else if (k > p + 1) {
                p = partition(arr, p + 1, right);
            }
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

以上代码在K为2时,可以正常求出第2大的数为6,。但是当K为4时,会发生死循环。

那么为什么发生死循环:

思路1:

仔细查看while循环中 if 和 else if 中的代码,分析其执行过程如下:

上图是每一次执行完 partition 方法后,基准点的下标 p 的位置。通过分析第9次和第14次的执行结果,可以发现如果继续往下走就会进入9-13次的死循环。现在已经发现了问题,那么造成问题的原因是什么?

2个问题导致的:

     1. while循环中left 和  right 的值一直没发生变化

     2. 通过分析第8次的执行结果,可以发现2个相同的值5在比较时不会发生数据交换,这导致7, 6, 5, 4, 5, 3, 3, 2, 1 中的第4个数和第5个数无法形成有序的位置。

思路2:

      将while循环中的代码和快排中的代码对比会发现,在循环中 left 和  right 的值一直没发生变化的。初步估计会导致已经排过序的数下次循环会继续参与排序,从而导致死循环。

1.0小结:

主要是想通过复制粘贴的方式快速实现第K大数的方案,偷懒的心态导致了此版本中的死循环问题

 1.1

··修复1.0版本中的死循环问题:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if (k < p + 1) {
                p = partition(arr, left, p - 1);
            } else if (k > p + 1) {
                p = partition(arr, p + 1, right);
            }
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] >= pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

修改了 partition 方法中第4行的代码,从 arr[arrIndex] > pivot 修改为 arr[arrIndex] >= pivot 。也就是在2个数相等时依然可以进行比较数据交换。

 2.0

··修复1.0版本中的while循环中left 和  right 的值一直没发生变化导致死循环问题,也是我们的主线版本:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);

    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p = partition(arr, left, right);
        while (k != p + 1) {
            if(k<p+1){
                right=p-1;
            }else if(k>p+1){
                left=p+1;
            }
            p=partition(arr,left,right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

修改了 quickSort 方法中第5行和第7行的代码,将while循环中的 partition 方法调用放到了 if 外面,在 if 和 else if 中修改 left 和right 的值,这样使用快排计算第K大的数就基本实现了。

3.0:

这个版本是对2.0版本的优化:

public class KthNum {
    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int k = 2;
        k=4;
        int kNum = quickSort(arr, 0, arr.length - 1, k);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[], int left, int right, int k) {
        if (left >= right) return -1;
        int p=-1;
        while (k != p + 1) {
            if(k<p+1){
                right=p-1;
            }else if(k>p+1){
                left=p+1;
            }
            p=partition(arr,left,right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

修改了 quickSort 方法中第2行的代码,这里的思路是 quickSort  方法中的第2行和第9行都调用了 partition 方法,我希望可以把它们统一起来,于是我给p赋值为-1,统一调用while循环中的partition方法。

4.0:

这个版本是对3.0版本的调整优化:

public class KthNum {
    public static int k = 4;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

优化点:

    1. 将 quickSort 方法的局部参数 left 和 right 放到方法内部赋值

    2. 移除了原quickSort 方法中 left 和 right  的比较

    3. quickSort 方法中添加了 K 值合理性的判断

4.0小结发散思考:

       至此,快排计算第K大的数就完成了。我们还是发散思考一下,既然第K大的数,那么肯定也有求第K小的数的需求。带着这个问题,我们重新审视一下上述4.0的代码,可以得出2种方案:

       1. 既然求第K大的数在while循环中可以通过K和P+1,在不同的区间范围排序查找。那么同理,求第K小的数也可以通过K和P的关系算出来。

       2. 上述 partition 方法中是通过倒序的方式求第K大的数,那么求第K小的数只需要采用升序的方式即可。

4.1:

这个版本就是4.0小结中的求第K小的数的第1种解决方案:

public class KthNum {
    public static int k = 3;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = bigK ? -1 : partition(arr, left, right);
        while (k != (bigK ? p + 1 : length - p)) {
            if (bigK ? k < p + 1 : k > length - p) {
                right = p - 1;
            } else if (bigK ? k > p + 1 : k < length - p) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (arr[arrIndex] > pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

代码的改动是涉及K和P的关系的比较,属于一个逆势思维。在一串倒序的数中求第K小的数,例如在7, 6, 5, 5, 4, 3, 3, 2, 1 这组数中6是第2大的数,是第8小的数。有兴趣的可以看下,不过重点推荐5.0版本的实现方案。

5.0:

这个版本就是4.0小结中的求第K小的数的第2种解决方案:

public class KthNum {
    public static int k = 2;
    public static boolean bigK = false;

    public static void main(String[] args) {
        int arr[] = {3, 2, 3, 1, 7, 4, 5, 5, 6};
        int kNum = quickSort(arr);
        System.out.println("kNum=" + kNum);
    }

    public static int quickSort(int arr[]) {
        int length = arr.length;
        if (k <= 0 || k > length) throw new RuntimeException("K值不合理");
        int left = 0, right = length - 1;
        int p = -1;
        while (k != p + 1) {
            if (k < p + 1) {
                right = p - 1;
            } else if (k > p + 1) {
                left = p + 1;
            }
            p = partition(arr, left, right);
        }
        return arr[p];
    }

    public static int partition(int[] arr, int left, int right) {
        int pivot = arr[right];
        int sortIndex = left;
        for (int arrIndex = sortIndex; arrIndex < right; arrIndex++) {
            if (bigK ? arr[arrIndex] > pivot : arr[arrIndex] < pivot) {
                swap(arr, arrIndex, sortIndex);
                sortIndex++;
            }
        }
        swap(arr, sortIndex, right);
        return sortIndex;
    }

    public static void swap(int[] arr, int i, int j) {
        if (i == j) return;
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


}

这里新增了一个成员变量 bigK 来做标识:true代表求第K大的数;false代表求第K小的数。同时在 partition 方法中的第4行也做了改动:若bigK为true,则采用倒序;若bigK为false,则采用升序。这样就以最小的改动实现了求第K大的数和第K小的数的需求,完美。

 

  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值