第k大问题各类变种小结

0

第k大问题各类变种小结

声明

文章均为本人技术笔记,转载请注明出处:
[1] https://segmentfault.com/u/yzwall
[2] blog.csdn.net/j_dark/

1. 求第k大数

  • lintcode 5 Kth Largest Element

  • lintcode 606 Kth Largest Element II

  • lintcode 461 Kth Smallest Numbers in Unsorted Array(第k小问题,切分时与求第k大相反)

1.1 解法1:Partition $O(n)$复杂度解法

1.1.1 分治法思想

参考快速排序中的切分思想,切分花费$O(n)$时间,分治将问题规模缩小为$T(frac{n}{2})$
$$T(n) = O(n) + T(n / 2) = O(n)$$

求第k大数问题,找到轴点$pivot$,在区间[start, end]中,

  1. 切分(partition)操作:求第k大数问题,将所有比$pivot$小的放在$pivot左侧$,小的放在右侧(求第k小问题时相反);
    切分完成后有$rightIndex + 1 = leftIndex$;

切分后区间[start, end]由三部分组成:

  1. 分治

    • 第K大元素在$pivot$左边区间nums[start]~nums[rightIndex],缩小问题规模

    • 第K大元素在$pivot$右边区间nums[leftIndex]~nums[end],缩小问题规模

    • 第K大元素在nums[rightIndex]~nums[leftIndex]之间,直接返回

/**
 * 解法1:快速排序切分思想求第K大元素,时间复杂度O(n),空间复杂度O(1)
 * http://www.lintcode.com/zh-cn/problem/kth-largest-element/
 * http://www.lintcode.com/zh-cn/problem/kth-largest-element-ii/
 * @author yzwall
 */
class Solution {
    public int kthLargestElement(int k, int[] nums) {
        if (nums == null) {
            return -1;
        }
        return quickSelect(nums, 0, nums.length - 1, k);
    } 
    /**
     * 快速排序切分思想求第k大元素,平均时间复杂度O(n)
     */
    private int quickSelect(int[] nums, int start, int end, int k) {
        if (start == end) {
            return nums[start];
        }
        
        int leftIndex = start;
        int rightIndex = end;
        // 切分尽量均匀
        int pivot = nums[start + (end - start) / 2];
        while (leftIndex <= rightIndex) {
            while (leftIndex <= rightIndex && nums[leftIndex] > pivot) {
                leftIndex++;
            }
            while (leftIndex <= rightIndex && nums[rightIndex] < pivot) {
                rightIndex--;
            }
            if (leftIndex <= rightIndex) {
                int temp = nums[leftIndex]; 
                nums[leftIndex] = nums[rightIndex];
                nums[rightIndex] = temp;
                leftIndex++;
                rightIndex--;
            }
        }
        
        // 第K大元素在pivot左边, 缩小问题规模
        if (start + k - 1 <= rightIndex) {
            return quickSelect(nums, start, rightIndex, k);
        }
        // 第K大元素在pivot右边, 缩小问题规模
        if (start + k - 1 >= leftIndex) {
            return quickSelect(nums, leftIndex, end, k - (leftIndex - start));
        }
        // 第K大元素在nums[rightIndex]~nums[leftIndex]之间
        return nums[leftIndex - 1];
    }
}

1.2 解法2:排序 $O(nlog n)$复杂度解法

升序排序后,直接输出第k大元素;

1.3 解法3:最小堆 $O(nlog n )$复杂度解法

将问题“求数组中第k大数”转换为“求数组中最大的k个数”
已知数组长度长度为$n$,维护节点总数为$k$的最小堆$heap$,堆顶即为第k大元素;

public int topK(int[] nums) {
    Heap heap = new Heap();
    for (int i = 0; i < nums.length; i++) {
        if (heap.size() < k) {
            heap.offer(nums[i]);
        } else {
            if (heap.top() < nums[i]){
                heap.poll();
                heap.offer(nums[i]);
            }
        }
    }
    return heap.top();
}

1.4 解法对比

--- Partition解法1 排序常规解法2 最小堆解法3
修改原数组
时间复杂度 $O(n)$ $O(nlog n)$ $O(nlog k)$
空间复杂度 $O(1)$ $O(1)$ $O(n)$

2 求第k小数

  • lintcode 461 Kth Smallest Numbers in Unsorted Array

求第k小数可用求第k大数三种解法解决,以下为$O(n)$复杂度代码

/**
 * 求第k小问题,快速排序切分思想解决,时间复杂度O(n),空间复杂度O(1)
 * http://www.lintcode.com/en/problem/kth-smallest-numbers-in-unsorted-array/
 * @author yzwall
 */
class Solution {
    public int kthSmallest(int k, int[] nums) {
        if (nums == null) {
            return -1;
        }
        return quickSelect(nums, 0, nums.length - 1, k);
    }
    
    private int quickSelect(int[] nums, int start, int end, int k) {
        if (start == end) {
            return nums[start];
        }
        
        int leftIndex = start;
        int rightIndex = end;
        int pivot = nums[start + (end - start) / 2];
        while (leftIndex <= rightIndex) {
            while (leftIndex <= rightIndex && nums[leftIndex] < pivot) {
                leftIndex++;
            }
            while (leftIndex <= rightIndex && nums[rightIndex] > pivot) {
                rightIndex--;
            }
            if (leftIndex <= rightIndex) {
                int temp = nums[leftIndex];
                nums[leftIndex] = nums[rightIndex];
                nums[rightIndex] = temp;
                leftIndex++;
                rightIndex--;
            }
        }
        
        if (start + k - 1 <= rightIndex) {
            return quickSelect(nums, start, rightIndex, k);
        }
        if (start + k - 1 >= leftIndex) {
            return quickSelect(nums, leftIndex, end, k - (leftIndex - start));
            
        }
        return nums[leftIndex - 1];
    }
}

3 求中位数/求数组中出现次数超过一半的数字

  • lintcode 80 Median

核心思想:已知数组长度长度为$n$,中位数本质为:
1 从小到大第$[frac{n}{2}]+1$大个数;
2 数组中出现次数超过一半的数字(剑指offer_面试题29);

3.1 解法1:$O(nlog n)$复杂度解法

将数组升序排序(花费时间复杂度$O(nlog n)$),$O(1)$时间复杂度直接输出第$[frac{n}{2}]+1$个数

3.2 解法2:$O(n)$复杂度解法

根据LintCode80_Median题意,已知数组长度长度为$n$,中位数等同为第$[frac{n}{2}]+1$大个数,直接转换为求第k大数问题求解,无需排序,时间复杂度为$O(n)$;

/**
 * 将求中位数转换为求第k大数问题,时间复杂度O(n)
 * http://www.lintcode.com/zh-cn/problem/median/
 * @author yzwall
 */
class Solution {
    public int median(int[] nums) {
        if (nums == null) {
            return -1;
        }
        // 中位数是从大到小第N/2 + 1大数
        int k = (nums.length / 2) + 1;
        return quickSelect(nums, 0, nums.length - 1, k);
    }
    
    private int quickSelect(int[] nums, int start, int end, int k) {
        if (start == end) {
            return nums[start];
        }
        
        int leftIndex = start;
        int rightIndex = end;
        int pivot = nums[start + (end - start) / 2];
        while (leftIndex <= rightIndex) {
            while (leftIndex <= rightIndex && nums[leftIndex] > pivot) {
                leftIndex++;
            }
            while (leftIndex <= rightIndex && nums[rightIndex] < pivot) {
                rightIndex--;
            }
            if (leftIndex <= rightIndex) {
                int temp = nums[leftIndex];
                nums[leftIndex] = nums[rightIndex];
                nums[rightIndex] = temp;
                leftIndex++;
                rightIndex--;
            }
        }
        
        if (start + k - 1 <= rightIndex) {
            return quickSelect(nums, start, rightIndex, k);
        }
        if (start + k - 1 >= leftIndex) {
            return quickSelect(nums, leftIndex, end, k - (leftIndex - start));
        }
        return nums[leftIndex - 1];
    }
}

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...