TopK问题求解方案讨论(时间复杂度,空间复杂度对比)

使用快速排序

  快排中的每一次循环,都是将数据分为两组,其中一组的任何数都小于另一组中的任何数,再不断地对数据进行分割直到不能再分即完成排序,其平均时间复杂度为O(n*log2n)
  快排求TopK思路:假设n个数存储在数组S中,从S中找出一个元素X,它把数组分为比它大的和比它小的两部分,假设比它大的一部分数组是Smax,比它小的部分是Smin。
这时有两种可能:
  1. Smax中元素不足K个,说明Smax中的所有数和Smin中最大的K-|Smax|个元素就是数组S中最大的K个数;
  2. Smax中元素的个数大于或等于K,则需要返回Smax中最大的K个元素。
优 点: 该算法的最优时间复杂度可以为O( k*log2n ),空间复杂度为O( 1 )
缺 点: 由于需要修改数组中的元素,所以需要将数组载入内存中,当数组元素过多时,需要考虑内存的使用问题。

 public static void main(String[] args) {
        /*start     使用快排找到TopK
        时间复杂度大于等于O( k*log(n) ),小于O( n*log(n) )      n为数组长,k为输入的参数
        空间复杂度为O(1)
        */
        int k = 7;//从1开始
        int[] arr = new int[]{4, 3, 6, 9, 3, 2, 0, 8};
        quickSortTopK(arr, 0, arr.length - 1, k);

        System.out.println("第" + k + "大元素为" + (k > arr.length ? "null" : arr[k - 1]));
        System.out.println(Arrays.toString(arr));//这里得到的数组不一定是排好序的,排序是否完成和k有关
        //end   使用快排找到TopK
    }

    private static void quickSortTopK(int[] arr, int left, int right, int k) {
        if (k - 1 > right) {
            return;
        }
        if (left >= right) {//查找结束
            return;
        }
        //分治法,按pivot将数组分成两部分达到分治的效果,该方法按降序分区数组
        int pivot = partition(arr, left, right);
        if (pivot == k) {//此处的pivot虽然不会在比较k次后刚好为k,但循环过程中,一定会拆分到k==pivot的位置
            return;
        }
        quickSortTopK(arr, left, pivot - 1, k);
        quickSortTopK(arr, pivot + 1, right, k);
    }

    private static int partition(int[] arr, int left, int right) {
        int pivot = arr[left];
        int pivotIndex = left;
        while (left <= right) {//一轮比较结束
            //循环从后向前比较pivot。如果大于pivot,end--;如果小于pivot,right位置值填入坑,并作为新坑,旧坑位置比较完成(left++),跳出
            while (left <= right) {
                if (pivot < arr[right]) {
                    pivotIndex = right;
                    arr[left] = arr[right];
                    left++;
                    break;
                }
                right--;
            }
            //循环从前往后比较pivot,如果大于pivot,start++;如果小于pivot,left位置值填入坑,并作为新坑,旧坑位置比较完成(right--),跳出
            while (left <= right) {
                if (arr[left] < pivot) {
                    pivotIndex = left;
                    arr[right] = arr[left];
                    right--;
                    break;
                }
                left++;
            }
        }
        arr[pivotIndex] = pivot;
        return pivotIndex;
    }

使用小顶堆

时间复杂度分析:

  1. 构建堆的时间复杂度O( k )
  2. 遍历原数组中剩余元素的时间复杂度为O( n-k )
  3. 调整堆的时间复杂度为O( log2k );

根据算法执行关系可得总的时间复杂度为O( k+(n-k)log2k ),k为输入的待寻找的位置,n为数组长度。
当k远小于n时,则可以认为该方法下的时间复杂度为O( nlog2k ),空间复杂度为O( 1 )
优点: 可以使用原数组的空间,空间复杂度低,排序性能相比于快速排序稳定。

public static void main(String[] args) {
        /**
         * start    使用小顶堆找TOPK
         * 时间复杂度为 O( n * log(k) ),空间复杂度为O( 1 )
         */
        int k = 8;//从1开始
        int[] arr = new int[]{4, 3, 6, 9, 3, 2, 7, 8};
        Integer i = heapSortTopK(arr, k);
        System.out.println("第" + k + "大元素为" + (i == null ? "null" : arr[i]));
        //end    使用小顶堆找TOPK
    }

    /**
     * 使用小顶堆寻找数组中的topK
     */
    private static Integer heapSortTopK(int[] arr, int k) {
        if (k > arr.length || k <= 0) {
            return null;
        }
        //在原数组上构建小顶堆
        builderHeap(arr, k);
        //比小顶堆堆顶元素大的数组元素,进入小顶堆
        for (int i = k; i < arr.length; i++) {
            //修改堆顶元素为当前数组中的元素
            if (arr[i] > arr[0]) {
                arr[0] = arr[i];
                downAdjust(arr, 0, k);
            }
        }
        return 0;
    }

    /**
     * 在数组中构建长为k的小顶堆
     */
    private static void builderHeap(int[] arr, int k) {
        //从最后一个非叶子节点调整,下沉元素
        for (int i = (k - 1) / 2; i >= 0; i--) {
            downAdjust(arr, i, k);
        }
    }

    /**
     * 循环下沉给定的parentIndex节点
     */
    private static void downAdjust(int[] arr, int parentIndex, int k) {
        //避免交换变量
        int temp = arr[parentIndex];
        int childIndex = parentIndex * 2 + 1;

        //下沉parentIndex
        while (childIndex < k) {
            //在数组长度范围内,拿到最小的孩子节点
            if (childIndex + 1 < k && arr[childIndex] > arr[childIndex + 1]) {
                childIndex++;
            }
 	//此处需要注意:1.父节点小于子节点,则证明不用下沉 2.跳出后续父子节点索引修改的操作,避免最后赋值(temp)的一步产生出错
            if (temp < arr[childIndex]) {
                //此处表示该节点已经不需要下沉了
                break;
            }
            //上浮子节点(因父节点已经被记录,则只需要赋值到父即可),更新父子节点的索引
            arr[parentIndex] = arr[childIndex];
            parentIndex = childIndex;
            childIndex = parentIndex * 2 + 1;
        }
        arr[parentIndex] = temp;
    }
发布了91 篇原创文章 · 获赞 54 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/BigBug_500/article/details/102558183