目录
左旋转字符串
1 题目描述
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
示例 1:
输入: s = “abcdefg”, k = 2
输出: “cdefgab”
示例 2:
输入: s = “lrloseumgh”, k = 6
输出: “umghlrlose”
限制:
1 <= k < s.length <= 10000
2 解题(Java)
2.1 解题思路
- 可以使用“字符串切片” , “StringBuilder遍历拼接” , “字符串遍历拼接” 三种方法;
- 本文采用“StringBuilder遍历拼接” 的方式解题,同时利用求余运算,可以简化代码;
2.2 代码
class Solution {
public String reverseLeftWords(String s, int n) {
StringBuilder res = new StringBuilder();
for(int i = n; i < n + s.length(); i++)
res.append(s.charAt(i % s.length()));
return res.toString();
}
}
3 复杂性分析
- 时间复杂度 O(N) : 线性遍历s并添加,使用O(N)时间;
- 空间复杂度 O(N) : 辅助res占用O(N)额外空间;
LRU 缓存机制
1 题目描述
运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。实现 LRUCache 类:
- LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
- int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。
- void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作?
示例:
输入
[“LRUCache”, “put”, “put”, “get”, “put”, “get”, “put”, “get”, “get”, “get”]
[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1],[3], [4]]
输出
[null, null, null, 1, null, -1, null, -1, 3, 4]解释
LRUCache lRUCache = new LRUCache(2);
lRUCache.put(1, 1); // 缓存是{1=1}
lRUCache.put(2, 2); // 缓存是 {1=1, 2=2}
lRUCache.get(1); // 返回1
lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3}
lRUCache.get(2); // 返回 -1 (未找到)
lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3}
lRUCache.get(1); // 返回 -1 (未找到)
lRUCache.get(3); // 返回 3
lRUCache.get(4); // 返回 4
提示:
- 1 <= capacity <= 3000
- 0 <= key <= 3000
- 0 <= value <= 10 ^ 4
- 最多调用 3 * 10 ^ 4 次 get 和 put
2 解题(Java)
1 哈希表 + 双向链表
- 双向链表按照被使用的顺序存储了键值对,靠近头部的键值对是最近使用的,靠近尾部的键值对是最久未使用的;
- 哈希表通过缓存数据的键映射到其在双向链表中的位置;
- 首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在O(1)时间内完成get或put操作;
- 对于get操作,首先判断key是否存在:如果key存在,先通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值;如果key不存在,返回-1;
- 对于put操作,首先判断key是否存在:如果key存在,先通过哈希表定位,再将对应的节点值更新为value,并将该节点移到双向链表的头部;如果key不存在,先判断哈希表的容量是否等于capacity,如果等于,先移除最后一个节点,然后使用key和value创建一个新节点,将key和该节点添加到哈希表中,并在双向链表的头部添加该节点;
- 在以上各项操作中,访问哈希表的时间复杂度为O(1),在双向链表的头部添加节点及在尾部删除节点的时间复杂度也为O(1);而将一个节点移到双向链表的头部,可以分成“在双向链表中删除该节点”和“在双向链表的头部添加节点”两步操作,都可以在O(1)时间内完成;
public class LRUCache {
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int capacity;
// 使用伪头部和伪尾部节点
private DLinkedNode head = new DLinkedNode();
private DLinkedNode tail = new DLinkedNode();
public LRUCache(int capacity) {
this.capacity = capacity;
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (cache.containsKey(key)) {
DLinkedNode node = cache.get(key);
moveToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
if (cache.containsKey(key)) {
DLinkedNode node = cache.get(key);
node.value = value;
moveToHead(node);
} else {
if (cache.size() == capacity) {
removeTail();
}
DLinkedNode node = new DLinkedNode(key, value);
cache.put(key, node);
addToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void moveToHead(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
addToHead(node);
}
private void removeTail() {
int rk = tail.prev.key;
tail.prev.prev.next = tail;
tail.prev = tail.prev.prev;
cache.remove(rk);
}
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
}
2 使用LinkedHashMap
class LRUCache extends LinkedHashMap<Integer, Integer>{
private int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75F, true);
this.capacity = capacity;
}
public int get(int key) {
return super.getOrDefault(key, -1);
}
public void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}
旋转图像
1 题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
示例 2:
输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]
示例 3:
输入:matrix = [[1]]
输出:[[1]]
示例 4:
输入:matrix = [[1,2],[3,4]]
输出:[[3,1],[4,2]]
提示:
- matrix.length == n
- matrix[i].length == n
- 1 <= n <= 20
- -1000 <= matrix[i][j] <= 1000
2 解题(Java)
将旋转转换为水平翻转+对角线翻转。
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 先水平翻转
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - 1 - i][j];
matrix[n - 1 - i][j] = temp;
}
}
// 再主对角线翻转
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
}
3 复杂性分析
- 时间复杂度O(N ^ 2):两次双层for循环,故为O(N ^ 2);
- 空间复杂度O(1):数组就地操作,故为O(1);
顺时针打印矩阵
1 题目描述
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
示例 2:
输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
限制:
0 <= matrix.length <= 100
0 <= matrix[i].length <= 100
2 解题(Java)
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0) return new int[0];
int left = 0, right = matrix[0].length - 1, top = 0, bottom = matrix.length - 1, x = 0;
int[] res = new int[(right + 1) * (bottom + 1)];
while(true) {
for(int i = left; i <= right; i++) res[x++] = matrix[top][i]; // left to right.
if(++top > bottom) break;
for(int i = top; i <= bottom; i++) res[x++] = matrix[i][right]; // top to bottom.
if(left > --right) break;
for(int i = right; i >= left; i--) res[x++] = matrix[bottom][i]; // right to left.
if(top > --bottom) break;
for(int i = bottom; i >= top; i--) res[x++] = matrix[i][left]; // bottom to top.
if(++left > right) break;
}
return res;
}
}
3 复杂性分析
- 时间复杂度 O(MN) :其中 M 和 N 分别是输入矩阵的行数和列数。矩阵中的每个元素都要被访问一次;
- 空间复杂度 O(MN):需要创建一个大小为 M*N的数组;
两数相加
1 题目描述
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
2 解题(Java)
2.1 思路
- 两链表从左到右同步遍历结点,初始化进位为0;
- 两结点的值与进位相加后取余,存储在新链表的结点中;两结点的值与进位相加后被10整除得进位;
- 如果有一个链表提前遍历结束,令其不再遍历,其结点的值始终为0,继续进行第二步的运算,直到两个链表都遍历结束;
- 如果进位不为0,还需新开辟一个结点,结点的值即为进位值;
2.2 代码
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, temp = null;
// carry代表进位数,初始为0,表示没有进位
int carry = 0;
// 当l1和l2都走到链表尾部,跳出循环
while (l1 != null || l2 != null) {
// 如果已经走到链表尾部,令值为0
int num1 = l1 != null ? l1.val : 0;
int num2 = l2 != null ? l2.val : 0;
// sum等于两数之和加进位
int sum = num1 + num2 + carry;
// 注意首次开辟新结点与后面开辟新结点有所区别
if (head == null) {
head = temp = new ListNode(sum % 10);
} else {
temp.next = new ListNode(sum % 10);
temp = temp.next;
}
// 获得本次运算的进位
carry = sum / 10;
// 如果结点没到链表尾,后移一个结点,否则不移
if (l1 != null) l1 = l1.next;
if (l2 != null) l2 = l2.next;
}
// 如果有进位,还需再开辟一个新节点
if (carry > 0) temp.next = new ListNode(carry);
return head;
}
}
3 复杂性分析
- 时间复杂度(O(max(M, N)):其中 M,N 为两个链表的长度,循环遍历次数为两个链表长度的大者,循环中的运算需要 O(1)的时间;
- 空间复杂度(O(max(M, N))*:新链表的长度最大为较长链表的长度 +1;
*栈的压入、弹出序列
1 题目描述
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
示例 1:
输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
输出:true
解释:我们可以按以下顺序执行:
push(1), push(2), push(3), push(4), pop() -> 4, push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
示例 2:
输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
输出:false
解释:1 不能在 2 之前弹出。
提示:
- 0 <= pushed.length == popped.length <= 1000
- 0 <= pushed[i],popped[i] < 1000
- pushed 是 popped 的排列。
2 解题(Java)
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Deque<Integer> stack = new LinkedList<>();
int i = 0;
for(int num : pushed) {
stack.push(num); // num 入栈
while(!stack.isEmpty() && stack.peek() == popped[i]) { // 循环判断与出栈
stack.pop();
i++;
}
}
return stack.isEmpty();
}
}
3 复杂性分析
- 时间复杂度O(N):其中N为数组pushed的长度,每个元素最多入栈与出栈一次,即最多共2N次出入栈操作;
- 空间复杂度O(N):辅助栈stack最多同时存储N个元素;
设计LRU缓存结构
1 题目描述
设计LRU缓存结构,该结构在构造时确定大小,假设大小为K,并有如下两个功能
- set(key, value):将记录(key, value)插入该结构
- get(key):返回key对应的value值
要求
- set和get方法的时间复杂度为O(1);
- 某个key的set或get操作一旦发生,认为这个key的记录成了最常使用的;
- 当缓存的大小超过K时,移除最不经常使用的记录,即set或get最久远的;
解释
- 若opt=1,接下来两个整数x, y,表示set(x, y);
- 若opt=2,接下来一个整数x,表示get(x),若x未出现过或已被移除,则返回-1;
- 对于每个opt2,输出一个答案;
示例1
输入
[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
返回值
[1,-1]
说明
第一次操作后:最常使用的记录为(“1”, 1) 第二次操作后:最常使用的记录为(“2”, 2),(“1”, 1)变为最不常用的
第三次操作后:最常使用的记录为(“3”, 2),(“1”, 1)还是最不常用的 第四次操作后:最常用的记录为(“1”, 1),(“2”, 2)变为最不常用的
第五次操作后:大小超过了3,所以移除此时最不常使用的记录(“2”, 2),加入记录(“4”,4),并且为最常使用的记录,然后(“3”, 2)变为最不常使用的记录
备注
2 解题(Java)
import java.util.*;
public class Solution {
private Map<Integer, DLinkedNode> cache = new HashMap<>();
private int k;
// 使用伪头部和伪尾部节点
private DLinkedNode head = new DLinkedNode();
private DLinkedNode tail = new DLinkedNode();
public int[] LRU (int[][] operators, int k) {
this.k = k;
head.next = tail;
tail.prev = head;
int len = 0;
for (int i=0; i<operators.length; i++) {
if (operators[i][0] == 2) len++;
}
int[] res = new int[len];
for(int i = 0, j = 0; i < operators.length; i++) {
if(operators[i][0] == 1) {
set(operators[i][1], operators[i][2]);
} else {
res[j++] = get(operators[i][1]);
}
}
return res;
}
public int get(int key) {
if (cache.containsKey(key)) {
DLinkedNode node = cache.get(key);
moveToHead(node);
return node.value;
}
return -1;
}
public void set(int key, int value) {
if (cache.containsKey(key)) {
DLinkedNode node = cache.get(key);
node.value = value;
moveToHead(node);
} else {
if (cache.size() == k) {
removeTail();
}
DLinkedNode node = new DLinkedNode(key, value);
cache.put(key, node);
addToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void moveToHead(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
addToHead(node);
}
private void removeTail() {
int rk = tail.prev.key;
tail.prev.prev.next = tail;
tail.prev = tail.prev.prev;
cache.remove(rk);
}
class DLinkedNode {
int key, value;
DLinkedNode prev, next;
public DLinkedNode() {}
public DLinkedNode(int key, int value) {
this.key = key;
this.value = value;
}
}
}