每日算法练习——LeetCode 347、222

LeetCode 347. 前 K 个高频元素

347. 前 K 个高频元素 - 力扣(LeetCode)

给你一个整数数组 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 rmid+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(rmid+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);
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值