小标题以 _
开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。
VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用
java main.java
运行(JDK 11 以后)
LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。
go get github.com/emirpasic/gods
栈与队列(简单)
1. 用两个栈实现队列
_解法 1:暴力做法
思路:
- 每次入栈就入 A 栈
- 出栈就将 A 全部丢到 B 再出,结束后将数据放回 A 栈
Java:
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void appendTail(int value) {
stack1.push(value);
}
public int deleteHead() {
while(!stack1.isEmpty()) {
stack2.push(stack1.pop());
}
Integer result = -1;
if (!stack2.isEmpty()) {
result = stack2.pop();
}
while(!stack2.isEmpty()) {
stack1.push(stack2.pop());
}
return result;
}
}
解法 2:优化解法 1
参考:清晰图解
思路:
- 优化掉解法 1 中出栈思路,无需每次出完就将数据放回 A 栈
Java:
class CQueue {
Stack<Integer> inStack, outStack;
public CQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}
public void appendTail(int value) {
inStack.push(value);
}
public int deleteHead() {
if (!outStack.isEmpty())
return outStack.pop();
if (inStack.isEmpty())
return -1;
while (!inStack.isEmpty())
outStack.push(inStack.pop());
return outStack.pop();
}
}
Go:
Go 里面没有 栈 这个数据结构,做这种题目有点麻烦… 以后数据结构题目还是用 Java 吧
type CQueue struct {
inStack, outStack *list.List
}
func Constructor() CQueue {
return CQueue{
inStack: list.New(),
outStack: list.New(),
}
}
func (this *CQueue) AppendTail(value int) {
this.inStack.PushBack(value)
}
func (this *CQueue) DeleteHead() int {
if this.outStack.Len() != 0 {
e := this.outStack.Back()
this.outStack.Remove(e)
return e.Value.(int)
}
if this.inStack.Len() == 0 {
return -1
}
for this.inStack.Len() > 0 {
this.outStack.PushBack(this.inStack.Remove(this.inStack.Back()))
}
e := this.outStack.Back()
this.outStack.Remove(e)
return e.Value.(int)
}
2. 包含 min 函数的栈
_解法 1:pop() 复杂度 O(n)
Java:
class MinStack {
int[] vals;
int min = Integer.MAX_VALUE;
int index = 0;
public MinStack() {
vals = new int[20000];
}
public void push(int x) {
if (x < min) {
min = x;
}
vals[index++] = x;
}
public void pop() {
int val = vals[--index];
if (val == min) {
min = Integer.MAX_VALUE;
for (int i = 0; i < index; i++) {
if (vals[i] < min) {
min = vals[i];
}
}
}
}
public int top() {
return vals[index - 1];
}
public int min() {
return min;
}
}
解法 2:链表
class MinStack {
private Node head;
public MinStack() {
}
public void push(int x) {
if (head == null)
head = new Node(x, x, null);
else
head = new Node(x, Math.min(head.min, x), head);
}
public void pop() {
head = head.next;
}
public int top() {
return head.val;
}
public int min() {
return head.min;
}
class Node {
int val;
int min;
Node next;
public Node(int val, int min, Node next) {
this.val = val;
this.min = min;
this.next = next;
}
}
}
链表(简单)
3. 从尾到头打印链表
_解法 1:常规遍历
思路:
- 创建一个容量足够大的数组,遍历链表将值存到数组中
- 再将该数组中的值倒序存到另一个数组,即为结果
// java
public int[] reversePrint(ListNode head) {
int[] vals = new int[10001];
int len = 0;
while (head != null) {
vals[len++] = head.val;
head = head.next;
}
int[] res = new int[len];
int j = 0;
for (int k = len - 1; k >= 0; k--) {
res[j++] = vals[k];
}
return res;
}
// go
func reversePrint(head *ListNode) []int {
vals := make([]int, 0)
for head != nil {
vals = append(vals, head.Val)
head = head.Next
}
res := make([]int, 0)
for i := len(vals) - 1; i >= 0; i-- {
res = append(res, vals[i])
}
return res
}
解法 2:优化解法 1 的空间
思路:
- 解法 1 中创建了一个数组来存储第一次遍历链表的值,不需要这么做
- 直接复制一份链表,则不用开辟很大的数组,尽量减少空间
// java
public int[] reversePrint(ListNode head) {
ListNode node = head;
int len = 0;
while (node != null) {
len++;
node = node.next;
}
int[] nums = new int[len];
node = head;
for (int i = len - 1; i >=0; i--) {
nums[i] = node.val;
node = node.next;
}
return nums;
}
Go:类似的优化,第一次遍历不需要进行赋值操作,只需要获取到数组长度,即可在第二次循环倒序赋值。
// go
func reversePrint(head *ListNode) []int {
cn := 0
for p := head; p != nil; p = p.Next {
cn++
}
node := make([]int, cn)
for head != nil {
node[cn-1] = head.Val
head = head.Next
cn--
}
return node
}
解法 3:递归
Java:
class Solution {
ArrayList<Integer> tmp = new ArrayList<>();
public int[] reversePrint(ListNode head) {
recur(head);
int[] res = new int[tmp.size()];
for (int i = 0; i < res.length; i++) {
res[i] = tmp.get(i);
}
return res;
}
void recur(ListNode head) {
if (head == null) return;
recur(head.next);
tmp.add(head.val);
}
}
Go:像 Go 这种可以往切片后面直接添加元素的语言,递归实现起来更简洁
func reversePrint3(head *ListNode) []int {
if head == nil {
return nil
}
return append(reversePrint(head.Next), head.Val)
}
解法 4:辅助栈
链表是从前往后访问每个节点,而题目要求倒序输出,这种先入后出的需求可以借助栈
// javapublic int[] reversePrint3(ListNode head) { Stack<Integer> stack = new Stack<>(); while (head != null) { stack.push(head.val); head = head.next; } int[] res = new int[stack.size()]; for (int i = 0; i < res.length; i++) res[i] = stack.pop(); return res;}
4. 反转链表(递归)
_解法 1:辅助栈 + 迭代
思路:
- 遍历链表,将值添加到栈中
- 再遍历该栈并出栈,将出栈的值组成新的链表
// java
public ListNode reverseList(ListNode head) {
if (head == null) return null;
Stack<Integer> stack = new Stack<>();
while(head != null) {
stack.push(head.val);
head = head.next;
}
ListNode node = new ListNode(stack.pop());
ListNode res = node;
while (!stack.isEmpty()) {
node.next = new ListNode(stack.pop());
node = node.next;
}
return res;
}
解法 2:双指针
// java
public ListNode reverseList2(ListNode head) {
ListNode cur = head, pre = null;
while (cur != null) {
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
解法 3:递归 *
递归 1:
// java
public ListNode reverseList(ListNode head) {
if (head == null) return null; // 空节点返回后还是空节点
if (head.next == null) return head; // 一个节点反转后还是这个节点
ListNode newNode = reverseList(head.next); // 递归后继节点
head.next.next = head;
head.next = null;
return newNode;
}
递归 2:剑指 Offer 24. 反转链表(迭代 / 递归,清晰图解)
// java
public ListNode reverseList(ListNode head) {
return recur(head, null);
}
public ListNode recur(ListNode cur, ListNode pre) {
if (cur == null) return pre;
ListNode node = recur(cur.next, cur); // 递归后继节点
cur.next = pre; // 修改节点引用指向
return node;
}
5. 复杂链表的复制
本题链表节点定义:
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
_解法 1:暴力迭代
思路:
- 先复制出一个链表副本(处理好 next,random 有 null 指 null,不做其他处理)
- 再次迭代,去处理 random 的指向
// java
public Node copyRandomList(Node head) {
if (head == null) return null;
Node newNode = new Node(head.val);
// 保存两个链表的首指针
Node pHead = head, pNew = newNode;
while (pHead != null) {
pNew.next = pHead.next == null ? null : new Node(pHead.next.val);
if (pHead.random == null)
pNew.random = null;
pNew = pNew.next;
pHead = pHead.next;
}
// 恢复指针状态
pHead = head;
pNew = newNode;
while (pHead != null) {
// 寻找random节点
Node tmpNode = head, ptmpNode = newNode;
while (pHead.random != tmpNode) {
tmpNode = tmpNode.next;
ptmpNode = ptmpNode.next;
}
pNew.random = ptmpNode;
pNew = pNew.next;
pHead = pHead.next;
}
return newNode;
}
解法 2:哈希
思路:
- 在解法 1 的基础上,优化寻找 random 节点的过程(使用 HashMap 存放)
学习的人家更优雅的写法:
// java
public Node copyRandomList2(Node head) {
if (head == null) return null;
Map<Node, Node> map = new HashMap<>();
Node cur = head;
// 复制各节点,并建立 "原节点 -> 新节点" 的映射
while (cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
// 构建新的next和random指向
while (cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
解法 3:拼接 + 拆分
注:如果能想到这个思路,实际上编写代码的难点在于 “拆分两链表”。
// java
public Node copyRandomList3(Node head) {
if (head == null) return null;
Node cur = head;
// 1. 复制各节点,并构建拼接链表
while (cur != null) {
Node node = new Node(cur.val);
node.next = cur.next;
cur.next = node;
cur = cur.next.next;
}
// 2. 构建各新节点的 random 指向
cur = head;
while (cur != null) {
if (cur.random != null)
cur.next.random = cur.random.next;
// cur.next.random = cur.random == null ? null : cur.random.next;
cur = cur.next.next;
}
// 3,拆分两链表
cur = head.next;
Node pre = head, res = head.next;
while (cur.next != null) {
pre.next = pre.next.next;
cur.next = cur.next.next;
pre = pre.next;
cur = cur.next;
}
pre.next = null; // 单独处理原链表尾节点
return res;
}
字符串(简单)
6. 替换空格
该题第一反应:调库
func replaceSpace_(s string) string { return strings.ReplaceAll(s, " ", "%20") }
_解法 1:迭代
// java
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (c == ' ') sb.append("%20");
else sb.append(c);
}
return sb.toString();
}
循环中也可以这么写:
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') sb.append("%20"); else sb.append(s.charAt(i));}
解法 2 :数组
代码 1:会浪费空间
class Solution {
public String replaceSpace(String s) {
int n = s.length();
char[] newArr = new char[3 * n]; // 最坏情况,全是空格
int j = 0;
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (c == ' ') {
newArr[j++] = '%';
newArr[j++] = '2';
newArr[j++] = '0';
} else {
newArr[j++] = c;
}
}
return new String(newArr, 0, j);
}
}
代码 2:不浪费空间,有些许性能损耗
- 就是提前计算一下需要初始化数组的大小
int n = s.length();
int cnt = 0;
for (char c: s.toCharArray()) {
if (c == ' ') cnt++;
}
char[] newArr = new char[n + 2 * cnt]; // 不浪费空间// 后面一样
解法 3:原地修改
C++ 中字符串是可变的,因此可以实现空间复杂度为 O(1) 的解法。
7. 左旋转字符串
_解法 1:迭代
思路:遍历字符串,将 k 之前和之后的内容分别拼接出新字符串,遍历结束返回拼接的字符串
// go
func reverseLeftWords(s string, n int) string {
var pre, suffix string
for i, v := range s {
if i < n {
suffix += string(v) // 前缀
} else {
pre += string(v) // 后缀
}
}
return pre + suffix
}
_解法 2:缩小迭代范围
思路:只迭代传入的 n 这个范围,将拿到的数据往字符串后面放即可
// go
func reverseLeftWords2(s string, n int) string {
res := []byte(s)
for i := 0; i < n; i++ {
res = append(res, s[i])
}
return string(res[n:])
}
解法 3:字符串切片
// gofunc reverseLeftWords3(s string, n int) string { return s[n:] + s[:n]}
// Javapublic String reverseLeftWords(String s, int n) { return s.substring(n) + s.substring(0, n);}
查找算法(简单)
8. 数组中重复的数字
题目:数组中重复的数字
_解法 1:迭代 + map
// go
func findRepeatNumber(nums []int) int {
m := make(map[int]int)
for i := 0; i < len(nums); i++ {
if val, ok := m[nums[i]]; ok {
return val
}
m[nums[i]] = nums[i]
}
return 0
}
// java
public int findRepeatNumber(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
if (map.containsKey(nums[i])) {
return nums[i];
}
map.put(nums[i], nums[i]);
}
return 0;
}
_解法 2:迭代 + 数组
// go
func findRepeatNumber2(nums []int) int {
records := make([]int, len(nums))
for i := 0; i < len(nums); i++ {
records[nums[i]]++
if records[nums[i]] > 1 {
return nums[i]
}
}
return 0
}
// java
public int findRepeatNumber2(int[] nums) {
int[] records = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
records[nums[i]]++;
if (records[nums[i]] > 1) {
return nums[i];
}
}
return 0;
}
解法 3:迭代 + set
这个其实和我想的 “迭代 + map” 属于相同思路,但是这里用 set 这个数据结构更合适。
public int findRepeatNumber3(int[] nums) {
Set<Integer> dic = new HashSet<>();
for (int num : nums) {
if (dic.contains(num))
return num;
dic.add(num);
}
return 0;
}
Golang 中没有实现 set 这个数据结构,还是用 map。
解法 4:原地交换
原地交换的思路和 “解法 2 - 迭代 + 数组” 有点类似,都是借助于 nums 里的所有数字都在 0~n-1 的范围内
这个条件,这个条件使得 nums 里的值一定都可以放到对应长度的数组中,这也就是解法 2 的思路。
这里更高级的点在于不需要开辟新的空间,只要想着将 nums 数组中的值放到这个值对应的索引位置,这个数组最后必然会变的有序,而某个地方如果值已经对上,下次再想放进来就能发现重复了。
// go
func findRepeatNumber3(nums []int) int {
i := 0
for i < len(nums) {
if nums[i] == nums[nums[i]] {
i++
continue
}
tmp := nums[i]
nums[i] = nums[nums[i+1]]
nums[nums[i+1]] = tmp
}
return -1
}
// java
public int findRepeatNumber(int[] nums) {
int i = 0;
while(i < nums.length) {
if(nums[i] == i) {
i++;
continue;
}
if(nums[nums[i]] == nums[i]) return nums[i];
int tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
return -1;
}
9. 在排序数组中查找数字 I
题目:剑指 Offer 53 - I. 在排序数组中查找数字 I
_解法 1:迭代
思路:遍历一次数组即可
func search(nums []int, target int) int {
var count int
for _, v := range nums {
if v > target {
break
}
if target == v {
count++
}
}
return count
}
_解法 2:二分法
思路:二分查找,找到一个满足条件的数组,则往前往后继续寻找
func search2(nums []int, target int) int {
var count int
start, end := 0, len(nums)
for start < end {
mid := (start + end) / 2
if target < nums[mid] {
end = mid
} else if target > nums[mid] {
start = mid + 1
} else {
count++
// behind
idx := mid + 1
for idx < len(nums) {
if nums[idx] == target {
count++
idx++
} else {
break
}
}
// front
idx = mid - 1
for idx >= 0 {
if nums[idx] == target {
count++
idx--
} else {
break
}
}
return count
}
}
return 0
}
评论区的二分法,思路更简洁一些:
func search3(nums []int, target int) int {
left, right := 0, len(nums)-1 var count int for left < right {
mid := (left + right) / 2 if nums[mid] >= target {
right = mid } if nums[mid] < target {
left = mid + 1 } } for left < len(nums) && nums[left] == target {
count++ left++ } return count}
10. 0~n-1中缺失的数字(二分)
对于有序数组,都应该考虑 二分法搜索。
题目:剑指 Offer 53 - II. 0~n-1中缺失的数字
_解法 1:迭代
// gofunc missingNumber(nums []int) int { length := len(nums) if nums[0] != 0 { return 0 } if nums[length-1] == length-1 { return length } for i := 1; i < length; i++ { if nums[i]-nums[i-1] != 1 { return nums[i] - 1 } } return -1}
解法 2:二分
经验之谈:二分循环范围如何选定
while(i <= j)
搜索的是闭区间[i, j]
,闭区间内的每一个元素都会被搜索,循环退出时i = j + 1
while(i < j)
搜索的是区间[i, j)
,区间内除了 j 指向的每一个元素都会被搜索,循环退出时i = j
根据具体问题,先明确下希望搜索的范围再决定用哪种,有的用哪种都可以,有的只能用其中一种。
// 二分模板public int binarySearch(int l, int r) {int mid;while (l < r) { mid = (l + r) >> 1; if (check(mid)) { r = mid; } else { l = mid + 1; }}return l;}public int binarySearch(int l, int r) {int mid;while (l < r) { mid = (l + r + 1) >> 1; if (check(mid)) { l = mid; } else { r = mid - 1; }}return l;}
二分的经验:计算中点
正常写法:
mid = (left + right) / 2
上面的写法在 left 和 right 特别大的时候,会有整形溢出的风险,最好如下写:
mid = left + (right - left) >> 1
我的二分:
// 二分搜索func missingNumber2(nums []int) int { length := len(nums) // 首尾边界 if nums[0] != 0 { return 0 } if nums[length-1] == length-1 { return length } left, right := 0, length-1 for left <= right { mid := (left + right) / 2 if nums[mid] <= mid { left = mid + 1 } else if nums[mid] > mid { right = mid - 1 } if nums[mid+1]-nums[mid] != 1 { return nums[mid] + 1 } } return -1}
题解中的二分:
// gofunc missingNumber3(nums []int) int { left, right := 0, len(nums)-1 for left <= right { mid := (left + right) >> 1 if nums[mid] == mid { left = mid + 1 } else { right = mid - 1 } } return left}