《剑指 Offer I》刷题笔记 1 ~10 题
《剑指 Offer I》刷题笔记 1 ~10 题
- 栈与队列(简单)
- 1. 用两个栈实现队列
- _解法 1:暴力做法
- 解法 2:优化解法 1
- 2. 包含 min 函数的栈
- _解法 1:pop() 复杂度 O(n)
- 解法 2:链表
- 链表(简单)
- 3. 从尾到头打印链表
- _解法 1:常规遍历
- 解法 2:优化解法 1 的空间
- 解法 3:递归
- 解法 4:辅助栈
- 4. 反转链表(递归)
- _解法 1:辅助栈 + 迭代
- 解法 2:双指针
- 解法 3:递归 *
- 5. 复杂链表的复制
- _解法 1:暴力迭代
- 解法 2:哈希
- 解法 3:拼接 + 拆分
- 字符串(简单)
- 6. 替换空格
- _解法 1:迭代
- 解法 2 :数组
- 解法 3:原地修改
- 7. 左旋转字符串
- _解法 1:迭代
- _解法 2:缩小迭代范围
- 解法 3:字符串切片
- 查找算法(简单)
- 8. 数组中重复的数字
- _解法 1:迭代 + map
- _解法 2:迭代 + 数组
- 解法 3:迭代 + set
- 解法 4:原地交换
- 9. 在排序数组中查找数字 I
- _解法 1:迭代
- _解法 2:二分法
- 10. 0~n-1中缺失的数字(二分)
- _解法 1:迭代
- 解法 2:二分
小标题以 _
开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。
VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用
java main.java
运行(JDK 11 以后)
LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。
go get github.com/emirpasic/gods
栈与队列(简单)
1. 用两个栈实现队列
题目:剑指 Offer 09. 用两个栈实现队列
_解法 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 函数的栈
题目:剑指 Offer 30. 包含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. 从尾到头打印链表
题目:剑指 Offer 06. 从尾到头打印链表
_解法 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 := 0for p := head; p != nil; p = p.Next {cn++}node := make([]int, cn)for head != nil {node[cn-1] = head.Valhead = head.Nextcn--}return node
}
解法 3:递归
参考题解:面试题06. 从尾到头打印链表(递归法、辅助栈法,清晰图解)
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:辅助栈
参考题解:面试题06. 从尾到头打印链表(递归法、辅助栈法,清晰图解)
链表是从前往后访问每个节点,而题目要求倒序输出,这种先入后出的需求可以借助栈
// 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. 反转链表(递归)
题目:剑指 Offer 24. 反转链表
_解法 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:双指针
参考题解:剑指 Offer 24. 反转链表(迭代 / 递归,清晰图解)
// 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. 复杂链表的复制
题目:剑指 Offer 35. 复杂链表的复制
本题链表节点定义:
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 存放)
题解:剑指 Offer 35. 复杂链表的复制(哈希表 / 拼接与拆分,清晰图解)
学习的人家更优雅的写法:
// 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:拼接 + 拆分
题解:剑指 Offer 35. 复杂链表的复制(哈希表 / 拼接与拆分,清晰图解)
注:如果能想到这个思路,实际上编写代码的难点在于 “拆分两链表”。
// 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. 替换空格
题目:剑指 Offer 05. 替换空格
该题第一反应:调库
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:原地修改
参考:面试题05. 替换空格 (字符串修改,清晰图解)
C++ 中字符串是可变的,因此可以实现空间复杂度为 O(1) 的解法。
7. 左旋转字符串
题目:剑指 Offer 58 - II. 左旋转字符串
_解法 1:迭代
思路:遍历字符串,将 k 之前和之后的内容分别拼接出新字符串,遍历结束返回拼接的字符串
// go
func reverseLeftWords(s string, n int) string {var pre, suffix stringfor 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:字符串切片
题解:面试题58 - II. 左旋转字符串(切片 / 列表 / 字符串,清晰图解)
// 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
题解:剑指 Offer 03. 数组中重复的数字(哈希表 / 原地交换,清晰图解)
这个其实和我想的 “迭代 + 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:原地交换
题解:剑指 Offer 03. 数组中重复的数字(哈希表 / 原地交换,清晰图解)
原地交换的思路和 “解法 2 - 迭代 + 数组” 有点类似,都是借助于 nums 里的所有数字都在 0~n-1 的范围内
这个条件,这个条件使得 nums 里的值一定都可以放到对应长度的数组中,这也就是解法 2 的思路。
这里更高级的点在于不需要开辟新的空间,只要想着将 nums 数组中的值放到这个值对应的索引位置,这个数组最后必然会变的有序,而某个地方如果值已经对上,下次再想放进来就能发现重复了。
// go
func findRepeatNumber3(nums []int) int {i := 0for 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 intfor _, v := range nums {if v > target {break}if target == v {count++}}return count
}
_解法 2:二分法
思路:二分查找,找到一个满足条件的数组,则往前往后继续寻找
func search2(nums []int, target int) int {var count intstart, end := 0, len(nums)for start < end {mid := (start + end) / 2if target < nums[mid] {end = mid} else if target > nums[mid] {start = mid + 1} else {count++// behindidx := mid + 1for idx < len(nums) {if nums[idx] == target {count++idx++} else {break}}// frontidx = mid - 1for 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}
《剑指 Offer I》刷题笔记 1 ~10 题相关推荐
- 《剑指offer》刷题笔记(发散思维能力):求1+2+3+...+n
<剑指offer>刷题笔记(发散思维能力):求1+2+3+-+n 转载请注明作者和出处:http://blog.csdn.net/u011475210 代码地址:https://githu ...
- 《剑指offer》刷题——【链表】从尾到头打印链表
<剑指offer>刷题--[链表]-<从尾到头打印链表> 问题分析: 递归实现: 1. 无返回值 2. 有返回值(ArrayList) 问题分析: 从头到尾打印链表比较简单,那 ...
- 《剑指Offer》刷题之最小的K个数
<剑指Offer>刷题之最小的K个数 我不知道将去向何方,但我已在路上! 时光匆匆,虽未曾谋面,却相遇于斯,实在是莫大的缘分,感谢您的到访 ! 题目: 给定一个数组,找出其中最小的K个数. ...
- 《剑指offer》刷题总结
从三月初开始刷剑指offer上面的题,到现在花了近二十天的时间终于刷完了.应该说,掌握上面的技巧应付一些公司面试题和小公司的笔试题是完全没有问题的.之前参加一个公司笔试,算法题就有一题是剑指offer ...
- 【剑指Offer】个人学习笔记_41_数据流中的中位数
目录 题目: [剑指 Offer 41. 数据流中的中位数](https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lc ...
- 【剑指Offer】个人学习笔记_15_二进制中1的个数
目录 题目: [剑指 Offer 15. 二进制中1的个数](https://leetcode-cn.com/problems/er-jin-zhi-zhong-1de-ge-shu-lcof/) 题 ...
- 【剑指Offer】个人学习笔记_61_扑克牌中的顺子
目录 题目: [剑指 Offer 61. 扑克牌中的顺子](https://leetcode-cn.com/problems/bu-ke-pai-zhong-de-shun-zi-lcof/) 题目分 ...
- 【剑指Offer】个人学习笔记_46_把数字翻译成字符串
目录 题目: [剑指 Offer 46. 把数字翻译成字符串](https://leetcode-cn.com/problems/ba-shu-zi-fan-yi-cheng-zi-fu-chuan- ...
- 【剑指Offer】个人学习笔记_38_字符串的排列
目录 题目: [剑指 Offer 38. 字符串的排列](https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/) 题目分析 初始解 ...
最新文章
- 【怎样写代码】实现对象的复用 -- 享元模式(一):问题案例
- 程序员基本功02对象与内存控制
- 【云周刊】第128期:支撑千亿营收背后秘密——首届阿里巴巴研发效能嘉年华...
- 车辆行人识别训练与部署,EasyDL-Jetson Nano 端边云协作专场公开课
- iOS : 静态库(.framework)合并
- java的字符定义_Java字符串定义及常用方法
- 微信小程序前端登录模块设计
- Ubuntu firefox无法加载视频
- 写给XJTU计算机系大一大二的童鞋
- 单位根检验urdf_R语言时间序列函数整理[转]]
- ios工程广告添加:广告sdk、广告中介添加(出海App)
- win7计算机服务项,新萝卜家园win7旗舰版服务项的详解
- 【数据结构与算法】线性表的查找
- 操作系统教程第六版——第三章课后作业
- 毕业论文小论文查重吗?
- 2007版Excel创建的数据透视表并不能在2003版中使用
- 手把手教你使用Newstart HA
- 内网渗透-代理篇(reGeorg+Proxifier代理工具)
- 机器视觉:光源控制器专业词汇中英文详解
- Docker基础入门(基本命令)