LeetCode 347. 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
要求:算法复杂度必须优于 O ( n l o g n ) O(n~log~n) O(n log n)
方法一:利用大根堆
解题思路:先遍历一次整个数组,使用哈希表记录每个数的出现次数,然后将这些数据对<元素,出现次数>放入大根堆中(按出现次数排序),最后从大根堆中取前k个最大的元素返回。
堆的调整操作复杂度为 O ( l o g n ) O(log n) O(logn),所以整体复杂度为 O ( n l o g n ) O(nlog~n) O(nlog n).
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
int[] res = new int[k];
for (int num : nums) {
if (!map.containsKey(num)) {
map.put(num, 1);
} else {
map.put(num, map.get(num) + 1);
}
}
PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o2.getValue() - o1.getValue());
queue.addAll(map.entrySet());
while (k > 0) {
res[--k] = Objects.requireNonNull(queue.poll()).getKey();
}
return res;
}
}
方法二:利用小根堆
解题思路:把大根堆换成小根堆,不同的是:在插入小根堆前,需要判断是否需要进行本次插入:
- 当小根堆大小 < k:直接插入
- 当小根堆大小 = k:判断要插入的元素,是否比堆中最小元素大,如果是则删去小值、插入大值;否则不进行本次插入。
最终小根堆中的 k 个元素,就是要求的 Top K
由于我们剪枝了不必要的插入操作,所以整体复杂度就是 O ( n l o g k ) O(nlog~k) O(nlog k)
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) {
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count});
}
}
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];
}
return ret;
}
}
方法三:快排思路
解题思路:
我们可以使用基于快速排序的方法,求出「出现次数数组」的前 k k k 大的值。
在对数组 arr[l...r]
做快排的过程中,我们会把数组分成两部分:arr[l...mid - 1]
、arr[mid...r]
,使得 arr[l...mid - 1]
中的元素小于arr[mid]
,arr[mid...r]
中的元素大于等于arr[mid]
于是,我们根据
k
k
k 与右侧数组 arr[mid...r]
的长度(为
r
−
m
i
d
+
1
r - mid + 1
r−mid+1)之间的大小关系:
- 如果
r-mid+1 > k
,则数组arr[l...r]
前 k k k 大的值,就是arr[mid...r]
前 k k k 大的值。 - 如果
r-mid+1 < k
,则数组arr[l...r]
前 k k k 大的值,就是arr[mid...r]
加上arr[l, mid - 1]
中前 k − ( r − m i d + 1 ) k - (r-mid+1) k−(r−mid+1)大的值 - 如果
r-mid+1 == k
,则数组arr[l...r]
前 k k k 大的值,就是arr[mid...r]
时间复杂度: O ( N 2 ) O(N^2) O(N2),其中 N N N 为数组的长度。
设处理长度为 N N N 的数组的时间复杂度为 f ( N ) f(N) f(N)。由于处理的过程包括一次遍历和一次子分支的递归,最好情况下,有 f ( N ) = O ( N ) + f ( N / 2 ) f(N)=O(N)+f(N/2) f(N)=O(N)+f(N/2),根据 主定理,能够得到 f ( N ) = O ( N ) f(N)=O(N) f(N)=O(N)。
最坏情况下,每次取的中枢数组的元素都位于数组的两端,时间复杂度退化为 O ( N 2 ) O(N^2) O(N2)。但由于我们在每次递归的开始会先随机选取中枢元素,故出现最坏情况的概率很低。
平均情况下,时间复杂度为 O ( N ) O(N) O(N)。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
int[] res = new int[k];
for (int num : nums) {
if (!map.containsKey(num)) {
map.put(num, 1);
} else {
map.put(num, map.get(num) + 1);
}
}
int[][] arr = new int[map.size()][2];
int index = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
arr[index++] = new int[]{entry.getKey(), entry.getValue()};
}
// 快排获取前K个
index = 0;
int l = 0, r = arr.length - 1;
while (l <= r) {
int mid = quickTopK(arr, l, r);
if (r - mid + 1 == k) {
for (int i = mid; i <= r; i++) {
res[index++] = arr[i][0];
}
break;
} else if (r - mid + 1 > k) {
l = mid;
} else {
for (int i = mid; i <= r; i++) {
res[index++] = arr[i][0];
}
k = k - (r - mid + 1);
r = mid - 1;
}
}
return res;
}
public int quickTopK(int[][] arr, int l, int r) {
int cur = r--;
while (l <= r) {
if (arr[l][1] > arr[cur][1]) {
swap(arr, l, r--);
} else {
l++;
}
}
swap(arr, ++r, cur);
return r;
}
public void swap(int[][] arr, int a, int b) {
int[] tmp = arr[a];
arr[a] = arr[b];
arr[b] = tmp;
}
}
LeetCode 222. 完全二叉树的节点个数
222. 完全二叉树的节点个数 - 力扣(LeetCode)
给你一棵 完全二叉树 的根节点 root
,求出该树的节点个数。
要求:算法复杂度必须优于 O ( n ) O(n) O(n)
解题思路:解二叉树题目,首先想到的就是能不能用递归写,但这道题我第一眼看的时候,觉得递归写不出来,原因是我的思想被禁锢住了,总是想着左树递归、右树递归,但其实只递归一个子树就可以了。
完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。
对于情况一,可以直接用 2 树深度 − 1 2^{树深度} - 1 2树深度−1 来计算,注意这里根节点深度为1。
对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
class Solution {
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
TreeNode left = root.left;
TreeNode right = root.right;
int height = 1;
// 判断树是不是满二叉树
while (left != null && right != null) {
left = left.left;
right = right.right;
height++;
}
if (left == null) {
return ((int)Math.pow(2, height) - 1);
}
// 判断右树是不是满二叉树
right = root.right;
int i = height;
while (right != null) {
right = right.left;
i--;
}
if (i == 0) {
return (int)Math.pow(2, height) + countNodes(root.right);
} else {
return (int)Math.pow(2, height - 1) + countNodes(root.left);
}
}
}