Top K 问题就是从一个数组中找打最大的 K 个数。
解法一:排序,取连续 K 个数
将数组从大到小排序后,前面 K 个数就是 Top K。
该解法的时间复杂度在于排序。所选排序算法的不同,会影响到执行效率。
所以可以选一个时间复杂度为 O(n*lg(n)) 的算法,即该解法的时间复杂度为 O(n*lg(n))
解法二:局部排序,取连续 K 个数
这是对解法一的改进。即,在排序过程中,排出最大的 K 个数时就停止排序,不需要对整个数组进行排序。
大致过程:
- 扫描数组中的元素,把其中最大的数移到数组的最左边;
- 重复执行该操作 K 次,就得到了 Top K
- 注意每次都是针对右侧子数组进行排序,不包括上一次排序得到的最大数
该解法时间复杂度为 O(n*k)
解法三:先取 K 个数,遍历剩余元素,将其和这 K 个数进行比较,如果比这个 K 个数中的最小值大,则执行替换操作
这是对解法二的改进。即,不对 Top K 元素排序。
事实上,为了加快剩余元素和已有 K 个数的比较,会将 K 个数存放在一个小顶堆中。
也就是,从严格意义上说,Top K 是经过排序的,但是操作小顶堆的时间复杂度为 O(lg(k))。
该解法的时间复杂度为 O(n*lg(k))。
解法四:用减治法递归划分出 Top K
大致过程:
- 用首个元素将数组分成左右两部分,左边的都比它大,右边的都比它小;
- 如果左边刚好是 K 个数,它们就是 Top K;
- 如果左边多于 K 个数,则再对左边进行划分;
- 如果左边少于 K 个数,则再对右边进行划分作为补充;
- 最终递归得到 Top K
该解法的时间复杂度为 O(n)
List<Integer> getTopK(int[] arr, int k) {
int last;
if (k <= 0) {
last = -1;
} else if (k >= arr.length) {
last = arr.length - 1;
} else {
last = partition(arr, 0, arr.length - 1, k);
}
ArrayList<Integer> result = new ArrayList<>(last + 1);
for (int i = 0; i <= last; i++) {
result.add(arr[i]);
}
return result;
}
int partition(int[] arr, int low, int high, int k) {
if (k >= high - low + 1) {
return high;
}
int mid = low;
for (int i = low + 1; i <= high; i++) {
if (arr[i] > arr[mid]) {
pop(arr, mid, i);
mid++;
}
}
int largerCount = mid - low;
if (largerCount == k) {
return mid - 1;
} else if (largerCount + 1 == k) {
return mid;
} else if (largerCount == 0) {
return partition(arr, low + 1, high, k - 1);
} else if (largerCount > k) {
return partition(arr, low, mid - 1, k);
} else {
return partition(arr, mid, high, k - largerCount);
}
}
void pop(int[] arr, int first, int last) {
int tmp = arr[last];
for (int i = last; i > first; i--) {
arr[i] = arr[i - 1];
}
arr[first] = tmp;
}