寻找最小的k个数

文章介绍了几种寻找数组中最小k个数的算法,包括直接排序、快速选择、快速排序以及插入排序。重点讨论了快速排序和快速选择算法的时间复杂度,并提供了代码示例。此外,还提出了扩展练习,如处理两个数组的和数组的最小k个数问题。
摘要由CSDN通过智能技术生成

题目描述

输入n个整数,输出其中最小的k个。(此题也可引申为求最大的k个数或求最小/最大的第k个数,最小的第k个数为arr[k-1])。


分析与解法

1.直接对数组排序,for循环遍历输出。排序方法:Java自带的Array.sort()方法或快速排序方法。快速排序平均所费时间为n*logn,然后再遍历序列中前k个元素输出即可。因此,总的时间复杂度:O(n * log n)+O(k)=O(n * log n)。
    //先将数组排序然后找到最小的k个数
    public static void Sortselect(int[] a, int n) {
        //Arrays.sort(a);//用Java自带数组排序方法
        QuickSort(a, 0, a.length - 1);//用快排
        for (int i = 0; i < n; i++) {
            System.out.println(a[i]);
        }
        //System.out.println(a[a.length - n]);//第k小的元素
    }

快速排序方法:QuickSort(int[] arr, int begin, int end),该排序算法关键在于寻找到临界值int index =partition(int[] arr, int begin, int end)。有两种方法,1.1.左右指针法:一头一尾两个指针往中间扫描,如果头指针遇到的数比主元大且尾指针遇到的数比主元小,则交换头尾指针所分别指向的数字。定义两个变量left和right,left指向首,right指向尾。再设定一个关键值为key=arr[begin],若key设定为最左边则right先开始移动,反之则left先移动。right从后往前找到第一个不大于key的元素停止,left从前往后找到第一个不小于key的元素停止,交换left和right所对应的元素,直到left= end,while循环结束。然后交换begin(key)和left,返回left或right=index(临界值)。此时key左边全为小于它的元素,key右边全为大于它的元素。分为两个数组,arr[begin,index-1]和arr[index+1,end],然后递归调用这两个数组。

    public static int partition(int[] arr, int begin, int end) {
        int key = arr[begin];//若选取最左边为key,则end先开始走
        //定义left和right两个变量,left指向最左边,right指向最右边,来与key做比较
        int left = begin;
        int right = end;
        while (left < right) {
            while (arr[right] >= key && right > left) {
                right--;
            }
            while (arr[left] <= key && left < right) {
                left++;
            }
            swap(arr, left, right);
        }
        swap(arr, begin, left);
        return left;
    }

1.2.快慢指针法:快慢指针法。以最右边为基准,fast指针指向数组首位置,slow指针指向fast前一个位置。For循环遍历数组,fast指针指向小于基准的数时,slow+1,且slow不等于fast时交换,循环结束后,交换基准值arr.length-1与slow+1,返回slow+1=index。分别递归再进行左右两数组的排序。QuickSort(arr, begin, index1 - 1);QuickSort(arr, index1 + 1, end);

    //快排的快慢指针实现,先在数组中选择一个数字为比较值,把数组中的数字分为两部分,比选择的数字小的数字移到数组的左边,比选择的数字大的数字移到数组的右边。
    //以最右边为基准,fast指针指向小于基准的数时,slow+1且不等于fast,交换,循环结束后,交换基准值与slow+1,分别递归再进行左右两数组的排序
    public static int partition1(int[] arr, int begin, int end) {
        if (begin >= end) {
            return begin;
        }
        int slow = begin - 1;
        int pivot = arr[end];
        for (int fast = begin; fast < end; fast++) {
            if (arr[fast] < pivot) {
                slow++;
                if (slow != fast) {
                    swap(arr, fast, slow);
                }
            }
        }
        swap(arr, slow + 1, end);
        return slow + 1;
    }
    //用快速排序对数组排序
    //partition过程有以下两种实现:
    //一头一尾两个指针往中间扫描,如果头指针遇到的数比主元大且尾指针遇到的数比主元小,则交换头尾指针所分别指向的数字;
    //一前一后两个指针同时从左往右扫,如果前指针遇到的数比主元小,则后指针右移一位,然后交换各自所指向的数字。
    public static void QuickSort(int[] arr, int begin, int end) {
        int index = partition(arr, begin, end);
        //begin..left-1 left left+1...end
        if (index > begin) {          //边缘条件限制,必须加,否则会导致访问数组以外的地址,溢出
            QuickSort(arr, begin, index - 1);
        }
        if (index < end) {            //同上
            QuickSort(arr, index + 1, end);
        }
    }

    public static void QuickSort1(int[] arr, int begin, int end) {
        int index1 = partition1(arr, begin, end);
        QuickSort(arr, begin, index1 - 1);
        QuickSort(arr, index1 + 1, end);
    }

2.进一步想想,题目没有要求最小的k个数有序,也没要求最后n-k个数有序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交换排序。SwapSelect(int[] n, int k)For循环遍历将最先遍历到的k个数放入k数组,n1放入剩余的数据。先遍历k数组找出其中最大值kmax,下标为maxIndex,然后遍历n1数组,用kmax进行比较,若kmax>n1[i],交换元素, k[maxIndex]与n1[i],刷新k数组的最大值,后重复此过程。输出karr[k-1].找最大值需要遍历这k个数,时间复杂度为O(k),每次遍历,更新或不更新数组的所用的时间为O(k)O(0)。故整趟下来,时间复杂度为n*O(k)=O(n*k)

   //不排序,将n个数的数组分成k和n-k两个数组,先遍历找出k数组的最大值max,再遍历n-k数组,将元素x与max进行比较,若小于则替换;
    // 并回到第二步重新找出k个元素的数组中最大元素kmax‘;如果x >= kmax,则继续遍历不更新数组
    public static void SwapSelect(int[] n, int k) {
        int[] karr = new int[k];
        //把最先遍历到的k个数放入k数组
        for (int i = 0; i < k; i++) {
            karr[i] = n[i];
        }
        int[] n1 = new int[n.length - karr.length];//n1为n-k数组,存储剩余的数据
        System.arraycopy(n, k, n1, 0, n1.length);

        for (int i = 0; i < karr.length; i++) {
            int kmax = karr[0];
            //遍历找出k数组的最大值max
            int j, maxIndex = 0;
            for (j = 1; j < karr.length; j++) {
                if (karr[j] > kmax) {
                    kmax = karr[j];
                    maxIndex = j;
                }
            }
            //遍历n-k数组,将元素x与max进行比较,若小于则替换
            for (int b = 0; b < n1.length; b++) {
                if (kmax > n1[b]) {
                    //交换两个数组的元素
                    int temp = n1[b];
                    n1[b] = karr[maxIndex];
                    karr[maxIndex] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(karr));//最小的k个数
        //System.out.println(karr[k - 1]);
        //求第k小的数,先求出k数组的最大值kmax,再与n-k数组找到元素比较,把小于kmax的元素替换
        //求第k大的数,先求k数组的最小值,再比较,把大元素替换进来


    }

3.快排亲兄弟-快速选择算法。求第k小的数,该算法类主要逻辑在于类似于快排取出临界值p ,partition(int[] arr, int begin, int end),该方法返回临界值p,若k<p,那么最小的k个数一定在p的左边begin...p-1,若k>p,那么最小的k个数一定在p的右边p+..end.若相等,返回第k小的元素,arr[p-1];在平均情况下,时间复杂度为O(N)

    //快速选择算法求第k小的数
    //对比快排先找出临界点p,若p<k,那么k一定在p+1-end之间,反之则在0-p-1之间
    public static int findKLargest(int[] arr, int k) {
        int begin = 0, end = arr.length - 1;
        while (begin <= end) {
            int p = partition(arr, begin, end);
            if (p < k) {
                //第k小的元素在p+1...end
                begin = p + 1;
            } else if (p > k) {
                //第k小的元素在begin...p-1
                end = p - 1;
            } else {
                //找到第K小的元素
                return arr[p - 1];
            }
        }
        return -1;
    }

 

4. 扩展练习:输入是两个整数数组,他们任意两个数的和又可以组成一个数组,求这个和中前k个数怎么做?Get_SumArrayofmin(int[] a, int[] b, int k) A数组有n个元素,B数组有m个元素,那么他们的和就有n*m个结果,假设A数组和B数组都为有序数组,那么A[1]+B[1]<A[1]+B[2]<A[1]+B[3]<… ,A[2]+B[1]<A[2]+B[2]<A[2]+B[3]<…,那么就是求A[n]+B[m]数组最小的k个数。先将A,B数组排序,定义一个和数组,len=A.length*B.length。双重for循环遍历,将每一个结果存入数组。求该数组的前k个最小数(可采用以上三种方法)。

    //Excise1,输入两个整数数组,他们任意两个数的和又可以组成一个数组,求这个和中前k个数
    public static void Get_SumArrayofmin(int[] a, int[] b, int m) {
        //对两个数组排序
        QuickSort(a, 0, a.length - 1);
        QuickSort(b, 0, b.length - 1);
        int[] sums = new int[a.length * b.length];
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < b.length; j++) {
                stack.add(a[i] + b[j]);//利用栈来存储和数组
            }
        }
        while (!stack.empty()) {
            for (int i = 0; i < sums.length; i++) {
                sums[i] = stack.pop();//将栈中的数据存到数组
            }
        }
       System.out.println(findKLargest(sums, k));//采用快速选择解法
    }

 

5.类似于快慢指针,介绍插入排序算法。

插入排序将数组分为有序和无序两部分,将无序数组的元素取出,与有序数组中的元素比较并添加进去。在内层循环中,j一直和j+1所指向的元素进行比较,若大于,则进行类似于冒泡排序的交换,j又指向有序数组的末尾元素,j+1则变成了原来j指向的元素。 for (int i = 1; i < arr.length; i++) {for (int j = i - 1; j >= 0; j--) { 

        if (arr[j] > arr[j + 1]) { 

            int temp = arr[j];

            arr[j] = arr[j + 1];

            arr[j + 1] = temp;

        } elsebreak;}}
刚开始有序数组放入第一个元素也是有序的因此i从1开始,j指向i-1,该位置是有序数组的末尾位置,要跟有序数组里面所有元素比较插入到适当位置,因此j--,j+1指向新加入的元素。
    //插入排序 314,298,508,123,486,145
    //将数组分为有序和无序两部分,将无序数组的元素取出,与有序数组中的元素比较并添加进去。
    //在内层循环中,j一直和j+1所指向的元素进行比较,若大于,则进行类似于冒泡排序的交换,j--,j指向有序数组末尾前一个位置的元素,j+1又指向了新元素,新元素与之前的元素进行对比
    public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0; j--) {//移动新元素到有序数组,j指向i-1,该位置是有序数组的末尾位置
                if (arr[j] > arr[j + 1]) {//j+1指向新加入的元素
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                } else
                    break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

月亮不打烊..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值