目录
- [180. 连续出现的数字](#180. 连续出现的数字)
- [68. 文本左右对齐](#68. 文本左右对齐)
- [146. LRU 缓存](#146. LRU 缓存)
180. 连续出现的数字
题目链接
表
- 表
Logs
的字段为id
和num
。
要求
- 找出所有 至少连续出现三次 的数字。
- 返回的结果表中的数据可以按 任意顺序 排列。
知识点
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. 文本左右对齐
题目链接
标签
数组 字符串 模拟
思路
有时候"困难"并不困难。就好比本题,只是解题的步骤多了点,而不是很难想。
题目中明确地说了要使用 贪心 的思想,即 通过局部最优解推出全局最优解 的思想,所以处理一行单词时不需要考虑上一行或下一行,只需要考虑如何让本行的单词数最多。这很简单,假设本行的单词之间只间隔一个空格,然后从文本中获取单词拼接即可。
不过不能直接按一个空格的间隔拼接这些单词,因为间隔一个空格只是假设,实际情况可能不满足(即可能出现 结果中这行的每个单词需要间隔一个以上空格 的情况),所以得先获取这行拼接的所有单词的长度总和,然后再计算每个单词之间的平均间隔,如果间隔不平均,则需要将这些不平均的空格分散到左边的每个单词后。
对于最后一行,需要特殊处理:使用一个空格将最后一行的每个单词拼接起来,然后用空格填满最后一行的剩余部分,最后将结果返回。
对于每行只有一个单词的情况,也需要特殊处理:将这个单词左对齐,即用空格填充这行的右部分。
废话不多说,请看代码中的注释。
代码
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 缓存
题目链接
标签
设计 哈希表 链表 双向链表
思路
本题是一个设计题,要求设计一个数据结构,它具有这样的性质:
- 如果插入操作导致关键字数量超过
capacity
,则应该 逐出 最久未使用的关键字 - 函数
get
和put
必须以 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;
}
}