题解 | #字符串出现次数的TopK问题#

字符串出现次数的TopK问题

http://www.nowcoder.com/practice/fd711bdfa0e840b381d7e1b82183b3ee

字符串出现次数的TopK问题

题目:给定一个字符串数组,再给定整数k,请返回出现次数前k名的字符串和对应的次数。返回的答案应该按字符串出现频率由高到低排序。如果不同的字符串有相同出现频率,按字典序排序。字符仅包含数字和字母

对于两个字符串,大小关系取决于两个字符串从左到右第一个不同字符的 ASCII 值的大小关系。比如"ah1x"小于"ahb","231"<”32“

例子:

输入:
["123","123","231","32"],2
复制
返回值:
[["123","2"],["231","1"]]
复制
说明:
 "123"出现了2次,记["123","2"],"231"与"32"各出现1次,但是"231"字典序在"32"前面,记["231","1"],最后返回[["123","2"],["231","1"]] 

分析:

  • 难点是词频相同时,词频高的排前面;词频不同时,字符字典序低的排前面

  • 用哈希表记录字符,其出现的次数;定义一个Node接受字符str和它对应的词频

    • Node implements Comparable<Node>重写compareTo

    • // 排序逻辑:排序在前面的,返回负数,排序在后面的,返回正数
      // 1.词频相同,词频高的在前,返回负数
      // 2.词频不相同,字典序低的在前,返回负数
      @Override
      public int compareTo(Node node) {
          if (this.times == node.times) {// 词频一样,比较字符串
              return this.str.compareTo(node.str);
          } else {// 词频不一样,就比较词频
              return node.times - this.times;
          }
      }
  • 生成k个长度的小根堆(排前面的CompareTo返回正数,排后面的CompareTo返回负数),来接受遍历到的strs中的字符串

    • 堆未满,就一直加入
    • 堆满了,堆满了,如果小根堆顶应该排在待插入元素的后面(>0),就更新堆顶
    • heapifyheapInsert自行看代码
  • 重新调整小根堆,使排序在前的放在堆顶

  • 结果集接受堆中数据,返回结果集

public class Solution {

    public static class Node implements Comparable<Node> {
        public String str;
        public int times;

        public Node(String str, int times) {
            this.str = str;
            this.times = times;
        }

        // 排序逻辑:排序在前面的,返回负数,排序在后面的,返回正数
        // 1.词频相同,词频高的在前,返回负数
        // 2.词频不相同,字典序低的在前,返回负数
        @Override
        public int compareTo(Node node) {
            if (this.times == node.times) {// 词频一样,比较字符串
                return this.str.compareTo(node.str);
            } else {// 词频不一样,就比较词频
                return node.times - this.times;
            }
        }
    }

    public String[][] topKstrings(String[] strings, int k) {
        if (strings == null || k < 1) {
            return new String[][]{};
        }
        // map统计每个字符,出现的词频
        HashMap<String, Integer> map = new HashMap<>();
        for (String s : strings) {
            if (!map.containsKey(s)) {
                map.put(s, 1);
            } else if (map.containsKey(s)) {
                map.put(s, map.get(s) + 1);
            }
        }
        // 遍历哈希表,每个node放入小根堆中
        // minHeap中的"小"指的是:排序放在后面的放在堆顶(compareTo返回正数)
        Node[] minHeap = new Node[k];
        int index = 0;
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            String str = entry.getKey();
            Integer times = entry.getValue();
            Node node = new Node(str, times);
            if (index < k) {// 堆未满,就加入
                minHeap[index] = node;
                heapInsert(minHeap, index++);
            } else {
                if (minHeap[0].compareTo(node) > 0) {// 堆满了,如果小根堆顶应该排在待插入元素的后面(>0),就更新堆顶
                    minHeap[0] = node;
                    heapify(minHeap, 0, k);
                }
            }
        }
        // 重新调整小根堆,使排序在前的放在堆顶
        for (int i = index - 1; i >= 0; i--) {
            swap(minHeap, 0, i);
            heapify(minHeap, 0, i);
        }
        // 结果集接受堆中数据,返回结果集
        String[][] res = new String[k][2];
        for (int i = 0; i < k; i++) {
            // 小根堆,就倒序放入res中
            res[i][0] = minHeap[i].str;
            res[i][1] = String.valueOf(minHeap[i].times);
        }
        return res;
    }

    private void heapify(Node[] minHeap, int index, int size) {
        while (2 * index + 1 < size) {
            int left = 2 * index + 1;
            // 越排在后面的,compareTo返回的越大,找出左右孩子节点中的排序后在更后面的
            if (left + 1 < size && minHeap[left].compareTo(minHeap[left + 1]) < 0) {
                left++;
            }
            // 如果父亲已经比孩子中最靠后的还要排序在后面,就停止本轮循环
            if (minHeap[index].compareTo(minHeap[left]) > 0) {
                break;
            }
            swap(minHeap, index, left);
            index = left;
        }
    }

    // 小根堆未满,加入堆时,排序小的往上放
    private void heapInsert(Node[] heap, int index) {
        while (index != 0) {
            int parent = (index - 1) / 2;
            // 返回正数,应该放后面,所以交换
            if (heap[index].compareTo(heap[parent]) > 0) {
                swap(heap, parent, index);
                index = parent;
            } else {
                break;
            }
        }
    }

    private void swap(Node[] minHeap, int i, int j) {
        Node temp = minHeap[i];
        minHeap[i] = minHeap[j];
        minHeap[j] = temp;
    }


    public static void main(String[] args) {
        Solution solution = new Solution();
        String[] strs = {"abcd", "abcd", "abcd", "pwb2", "abcd", "pwb2", "p12"};
        int k = 3;
        String[][] res = solution.topKstrings(strs, k);
        // 正确:[["abcd","4"],["pwb2","2"],["p12","1"]]
        System.out.println(Arrays.deepToString(res));
        System.out.println("---------");
        String s1 = "231";
        String s2 = "32";
        Node n1 = new Node(s1, 1);// 词频相同,231字典序<32,返回负数
        Node n2 = new Node(s2, 1);

        Node n3 = new Node(s1, 1);// 词频不相同,231词频<32词频,返回正数
        Node n4 = new Node(s2, 2);

        System.out.println(n1.compareTo(n2));// -1
        System.out.println(n3.compareTo(n4));// 1

    }
}
全部评论

相关推荐

刚刷到字节跳动官方发的消息,确实被这波阵仗吓了一跳。在大家还在纠结今年行情是不是又“寒冬”的时候,字节直接甩出了史上规模最大的转正实习计划——ByteIntern。咱们直接看几个最硬的数,别被花里胡哨的宣传词绕晕了。首先是“量大”。全球招7000多人是什么概念?这几乎是把很多中型互联网公司的总人数都给招进来了。最关键的是,这次的资源分配非常精准:研发岗给了4800多个Offer,占比直接超过六成。说白了,字节今年还是要死磕技术,尤其是产品和AI领域,这对于咱们写代码的同学来说,绝对是今年最厚的一块肥肉。其次是大家最关心的“转正率”。官方直接白纸黑字写了:整体转正率超过50%。这意味着只要你进去了,不划水、正常干,每两个人里就有一个能直接拿校招Offer。对于2027届(2026年9月到2027年8月毕业)的同学来说,这不仅是实习,这简直就是通往大厂的快捷通道。不过,我也得泼盆冷水。坑位多,不代表门槛低。字节的实习面试出了名的爱考算法和工程实操,尤其是今年重点倾斜AI方向,如果你简历里有和AI相关的项目,优势还是有的。而且,转正率50%也意味着剩下那50%的人是陪跑的,进去之后的考核压力肯定不小。一句话总结:&nbsp;27届的兄弟们,别犹豫了。今年字节这是铁了心要抢提前批的人才,现在投递就是占坑。与其等到明年秋招去千军万马挤独木桥,不如现在进去先占个工位,把转正名额攥在手里。
喵_coding:别逗了 50%转正率 仔细想想 就是转正与不转正
字节7000实习来了,你...
点赞 评论 收藏
分享
1jian10:48h没写面评会变成这样
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务