LeetCode 180, 68, 146

目录

180. 连续出现的数字

题目链接

180. 连续出现的数字

  • Logs的字段为idnum

要求

  • 找出所有 至少连续出现三次 的数字。
  • 返回的结果表中的数据可以按 任意顺序 排列。

知识点

  1. distinct:对某个字段进行去重。

思路

可以想到使用三个表来做多表查询,第一个表first查数字在三次出现中的第一次出现的id,第二个表second查第二次出现的id,第三个表third查数字在三次出现中的第三次出现的id,只要满足first.id = second.id - 1 and second.id = third.id - 1即可。

另外,因为是多表查询,所以要使用条件first.num = second.num and second.num = third.sum限制,防止产生无效数据。

最后,也是最关键的一点:如果一个num连续出现三次以上,则按照上面这种判断将会把这个num输出多次,故需要对num进行去重

代码

sql 复制代码
select
    distinct first.num ConsecutiveNums
from
    Logs first,
    Logs second,
    Logs third
where
    first.id = second.id - 1
and
    second.id = third.id - 1
and
    first.num = second.num
and
    second.num = third.num

68. 文本左右对齐

题目链接

68. 文本左右对齐

标签

数组 字符串 模拟

思路

有时候"困难"并不困难。就好比本题,只是解题的步骤多了点,而不是很难想。

题目中明确地说了要使用 贪心 的思想,即 通过局部最优解推出全局最优解 的思想,所以处理一行单词时不需要考虑上一行或下一行,只需要考虑如何让本行的单词数最多。这很简单,假设本行的单词之间只间隔一个空格,然后从文本中获取单词拼接即可。

不过不能直接按一个空格的间隔拼接这些单词,因为间隔一个空格只是假设,实际情况可能不满足(即可能出现 结果中这行的每个单词需要间隔一个以上空格 的情况),所以得先获取这行拼接的所有单词的长度总和,然后再计算每个单词之间的平均间隔,如果间隔不平均,则需要将这些不平均的空格分散到左边的每个单词后。

对于最后一行,需要特殊处理:使用一个空格将最后一行的每个单词拼接起来,然后用空格填满最后一行的剩余部分,最后将结果返回。

对于每行只有一个单词的情况,也需要特殊处理:将这个单词左对齐,即用空格填充这行的右部分。

废话不多说,请看代码中的注释。

代码

java 复制代码
class Solution {
    public List<String> fullJustify(String[] words, int maxWidth) {
        int n = words.length;
        List<String> res = new ArrayList<>();
        int right = 0; // right是每行已经拼接字符串的结尾单词的下标+1
        while (true) {
            int left = right; // left是每行起始单词的下标
            int sumLen = 0; // sumLen是这行的长度,不包括空格

            // 先计算出这行最多能放多少个单词
            // right - left 恰好是单词之间至少间隔的空格数,包含 已拼接的最后一个单词 与 待拼接单词 的空格
            // sumLen + words[right].length() + right - left 是这行已经填充的字符数
            // 如果right == n,说明单词已经放完了,就不需要再计算了
            while (right < n && sumLen + words[right].length() + right - left <= maxWidth) {
                sumLen += words[right++].length(); // 让sumLen加上这个单词的长度
            }

            // 如果right == n,拼接最后一行的字符串,将结果返回即可
            if (right == n) {
                StringBuilder builder = join(words, left, n, " "); // 用单个空格将这行单词拼接起来
                builder.append(blank(maxWidth - builder.length())); // 用空格填充最后一行的剩余部分
                res.add(builder.toString()); // 将最后一行字符串加入结果
                return res; // 返回结果
            }

            // 如果到这里了,说明不是最后一行
            int numWords = right - left; // right - left 可以表示这行拼接的单词数
            int numSpaces = maxWidth - sumLen; // numSpaces 是这行的空格数
            
            // 如果这行只有一个单词,那么直接将这个单词左对齐,看示例2的acknowledgment,它就是左对齐的
            if (numWords == 1) {
                StringBuilder builder = new StringBuilder(words[left]);
                builder.append(blank(numSpaces)); // 用空格填充这行的剩余部分
                res.add(builder.toString());
                continue;
            }

            // 如果到这里了,说明这行有不止一个单词
            // numWords - 1 是这行单词之间的间隔数
            int avgSpaces = numSpaces / (numWords - 1); // avgSpaces 是这行平均的空格数
            int extraSpaces = numSpaces % (numWords - 1); // extraSpaces 是无法平均的空格数

            // 题目中说了 左侧放置的空格数要多于右侧的空格数,所以把 不平均的空格 分散到 左边的每个单词后。即使不平均的空格数为0,也可以按照如下逻辑处理
            StringBuilder builder = new StringBuilder();
            builder.append(join(words, left, left + extraSpaces + 1, blank(avgSpaces + 1))); // 把不平均的空格分散到左边的每个单词后
            builder.append(blank(avgSpaces)); // 从这里开始空格就平均了,放 avgSpaces 个
            builder.append(join(words, left + extraSpaces + 1, right, blank(avgSpaces))); // 把平均的空格分散到右边的每个单词后
            res.add(builder.toString());
        }
    }
    // 构造n个空格的字符串
    private String blank(int n) {
        StringBuilder builder = new StringBuilder();
        while (n-- > 0) {
            builder.append(' ');
        }
        return builder.toString();
    }
    // 将 words中 索引在[left, right)这个左闭右开区间内的字符串 使用 interval 作为间隔 拼接起来
    private StringBuilder join(String[] words, int left, int right, String interval) {
        StringBuilder builder = new StringBuilder(words[left++]); // 先拼接一个字符串,再拼接间隔
        while (left < right) {
            builder.append(interval);
            builder.append(words[left++]);
        }
        return builder;
    }
}

146. LRU 缓存

题目链接

146. LRU 缓存

标签

设计 哈希表 链表 双向链表

思路

本题是一个设计题,要求设计一个数据结构,它具有这样的性质:

  • 如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字
  • 函数 getput 必须以 O ( 1 ) O(1) O(1) 的平均时间复杂度运行。

保证逐出最久的关键字很简单,使用队列即可。

但还要保证这个关键字是最久未使用的就比较麻烦了,其实仍然可以使用队列,不过在每次使用关键字时得把它放到队列的尾部。

但是如何把它放到队列尾部呢?实际上,把一个队列中的一个节点放到尾部可以先删除这个节点,再将其加入队列。

问题来了,如何以 O ( 1 ) O(1) O(1) 的平均时间复杂度 查询 和 删除 队列中的某一个节点呢?可以使用Map来映射,key为关键字,value为这个关键字在队列中的节点,在创建节点时就建立关键字与节点的映射,然后在查询和删除时使用关键字查出节点,接着就可以进行操作了。

要想以 O ( 1 ) O(1) O(1) 的平均时间复杂度删除队列中的某一个节点,需要将队列中的节点设置为双向节点,即这个节点即有指向后一个节点的指针,也有指向前一个节点的指针。

另外,为了避免对删除头节点和尾节点的判断,可以使用带哨兵节点的双向循环链表,即链表的头部和尾部都指向一个哨兵节点,这样在删除的时候直接 让前一个节点的后指针指向后一个节点、让后一个节点的前指针指向前一个节点 即可,如果没有带哨兵节点,则在 删除节点是头节点或尾节点 时,还需要让链表的头节点或尾节点重新指向另一个节点。

代码更好理解一点,请看代码。

代码

java 复制代码
class LRUCache {

    private static class Node {
        int key;
        int value;
        Node prev;
        Node next;
        public Node() {}
        public Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private static class DoublyLinkedList {
        Node head;
        Node tail;
        public DoublyLinkedList() {
            head = tail = new Node(); // 哨兵节点
            head.next = tail; // 头哨兵节点的后指针指向尾哨兵节点
            tail.prev = head; // 尾哨兵节点的前指针指向头哨兵节点
        }
        // 将新节点添加到队列尾部
        public void addLast(Node newLast) {
            Node oldLast = tail.prev; // 获取原来的尾节点
            newLast.prev = oldLast; // 让新节点的前指针指向原来的尾节点
            newLast.next = tail; // 让新节点的后指针指向尾哨兵节点
            tail.prev = newLast; // 让尾哨兵节点的前指针指向新节点
            oldLast.next = newLast; // 让原来的尾节点的后指针指向新节点
        }
        // 将指定节点从队列中移除
        public void remove(Node node) {
            Node prev = node.prev; // 获取前一个节点
            Node next = node.next; // 获取后一个节点
            prev.next = next; // 让前一个节点的后指针指向后一个节点
            next.prev = prev; // 让后一个节点的前指针指向前一个节点
        }
        // 移除队列中的第一个节点
        public Node removeFirst() {
            Node first = head.next; // 获取头节点
            remove(first); // 将头节点从队列中移除
            return first; // 返回头节点
        }
    }

    private final HashMap<Integer, Node> map = new HashMap<>();
    private final DoublyLinkedList list = new DoublyLinkedList();
    private final int capacity; // 容量

    public LRUCache(int capacity) {
        this.capacity = capacity;
    }
    
    public int get(int key) {
        // 如果没有这个元素,则返回-1
        if (!map.containsKey(key)) {
            return -1;
        }

        Node node = update(key); // 将节点更新到队列尾部
        return node.value; // 然后返回节点的值
    }
    
    public void put(int key, int value) {
        if (map.containsKey(key)) { // 如果有这个关键字,做更新操作
            Node node = update(key); // 将节点更新到队列尾部
            node.value = value; // 然后更新节点的值
        } else { // 否则没有这个关键字,做新增操作
            Node node = new Node(key, value); // 构造新节点
            map.put(key, node); // 建立映射
            list.addLast(node); // 将新节点加入队列尾部
            if (map.size() > capacity) { // 如果数量超出限制,则移除队列中的头节点
                Node removed = list.removeFirst();
                map.remove(removed.key); // 删除这个关键字与节点的映射
            }
        }
    }
    
    // 先删除再添加到尾部,实现将节点更新到尾部的效果
    private Node update(int key) {
        Node node = map.get(key);
        list.remove(node);
        list.addLast(node);
        return node;
    }
}
相关推荐
风铃儿~19 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组24 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿35 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA1 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
ai产品老杨1 小时前
减少交通拥堵、提高效率、改善交通安全的智慧交通开源了。
前端·vue.js·算法·ecmascript·音视频
gaoliheng0061 小时前
Redis看门狗机制
java·数据库·redis
我是唐青枫1 小时前
.NET AOT 详解
java·服务器·.net
小于不是小鱼呀1 小时前
手撕 K-Means
人工智能·算法·机器学习
m0_740154671 小时前
K-Means颜色变卦和渐变色
算法·机器学习·kmeans