《剑指 Offer I》刷题笔记 11 ~ 19 题
小标题以 _
开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。
VsCode 搭建的 Java 环境中 sourcePath 配置比较麻烦,可以用
java main.java
运行(JDK 11 以后)
Go 的数据结构:LeetCode 支持 https://godoc.org/github.com/emirpasic/gods 第三方库。
go get github.com/emirpasic/gods
查找算法(中等)
11. 二维数组中的查找
_解法 1:暴力迭代
时间复杂度:O(n * m)
// go
func findNumberIn2DArray(matrix [][]int, target int) bool {
for _, item := range matrix {
for _, v := range item {
if target == v {
return true
}
}
}
return false
}
解法 2:标志数
以后遇到二维数组,不一定要从 0, 0 开始循环,多点别的思路。。。
时间复杂度:O(n + m)
// go
func findNumberIn2DArray(matrix [][]int, target int) bool {
row := len(matrix) // []
if row == 0 {
return false
}
col := len(matrix[0]) // [[]]
if col == 0 {
return false
}
i, j := row-1, 0
for i >= 0 && j < col {
if matrix[i][j] > target {
i--
} else if matrix[i][j] < target {
j++
} else {
return true
}
}
return false
}
解法 3:逐行二分
还是没有领悟那句话的精髓 ---- 遇到有序数组就用二分!
思路:依旧是遍历每行,对行再遍历的时候使用二分查找
时间复杂度:O(n * log(m))
// java
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if (matrix.length == 0) {
return false;
}
for (int i = 0; i < matrix.length; i++) {
int index = Arrays.binarySearch(matrix[i], target);
if (index >= 0) {
return true;
}
}
return false;
}
}
12. 旋转数组的最小数字
_解法 1:暴力迭代
思路:遍历找到第一个比左边的数字大的数字,就是目标值
func minArray(numbers []int) int {
if len(numbers) == 1 {
return numbers[0]
}
for i := 1; i < len(numbers); i++ {
if numbers[i] < numbers[i-1] {
return numbers[i]
}
}
return numbers[0]
}
题解中看到的思路:遍历找到比 numbers[0] 小的值就是目标值
func minArray(numbers []int) int {
for i := 0; i < len(numbers); i++ {
if numbers[i] < numbers[0] {
return numbers[i]
}
}
return numbers[0]
}
解法 2:二分
二分的模板已经很熟了,但是 真正应用还需要多练!
核心在于在适当的时候控制边界条件!
func minArray(number []int) int {
left, right := 0, len(number)-1
for left < right {
mid := left + (right-left)>>1
if number[mid] < number[right] {
right = mid
} else if number[mid] > number[right] {
left = mid + 1
} else {
right--
}
}
return number[left]
}
func minArray(numbers []int) int {
left, right := 0, len(numbers)-1
for left < right {
mid := left + (right-left)>>1
if numbers[mid] < numbers[right] {
right--
} else if numbers[mid] > numbers[right] {
left = mid + 1
}
}
return numbers[left]
}
13. 第一个只出现一次的字符
_解法 1:暴力迭代
若从前往后找到某个元素的索引,和从后往前找到某个元素的索引相同,则说明只出现一次。
好像由于下面的方法每次要执行
s[i]
,还是会比上面慢一点。
// go
// 转成 byte 数组后遍历
func firstUniqChar(s string) byte {
b := []byte(s)
for _, v := range b {
if bytes.LastIndexByte(b, v) == bytes.IndexByte(b, v) {
return v
}
}
return ' '
}
// 直接遍历字符串
func firstUniqChar(s string) byte {
for i := 0; i < len(s); i++ {
if strings.IndexByte(s, s[i]) == strings.LastIndexByte(s, s[i]) {
return s[i]
}
}
return ' '
}
_解法 2:哈希
// go
func firstUniqChar2(s string) byte {
m := make(map[byte]int, 0)
b := []byte(s)
for _, v := range b {
m[v]++
}
for _, v := range b {
if m[v] == 1 {
return v
}
}
return ' '
}
评论中的做法:是哈希的思路,但是用数组存储。
对于这种存储范围已经固定的,完全可以用数组。
// go
func firstUniqChar3(s string) byte {
if s == "" {
return ' '
}
dic := make([]int, 26)
b := []byte(s)
for _, v := range b {
dic[v-'a']++
}
for _, v := range b {
if dic[v-'a'] == 1 {
return v
}
}
return ' '
}
解法 3:有序哈希表
题解:面试题50. 第一个只出现一次的字符(哈希表 / 有序哈希表,清晰图解)
该思路我是想到了,不过 Go 中没有 有序哈希表 这个数据结构,Java 中有 LinkedHashMap。
// java
public char firstUniqChar2(String s) {
Map<Character, Boolean> dic = new LinkedHashMap<>();
char[] sc = s.toCharArray();
for (char c : sc)
dic.put(c, !dic.containsKey(c));
for (Map.Entry<Character, Boolean> d : dic.entrySet()) {
if (d.getValue())
return d.getKey();
}
return ' ';
}
搜索与回溯算法(简单)
14. 从上到下打印二叉树 I
二叉树的层序遍历,又称二叉树的 广度优先搜索(BFS)。
广度优先搜索算法(Breadth-First-Search,缩写为 BFS),是一种利用 队列 实现的搜索算法。
简单来说,其搜索过程和 “湖面丢进一块石头激起层层涟漪” 类似。
深度优先搜索算法(Depth-First-Search,缩写为 DFS),是一种利用 递归 实现的搜索算法。
简单来说,其搜索过程和 “不撞南墙不回头” 类似。
解法 1:队列
// java
class Solution {
public int[] levelOrder(TreeNode root) {
if (root == null) return null;
List<Integer> resList = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>() {
{
offer(root); }};
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
resList.add(node.val);
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
}
// list ---> int[]
return resList.stream().mapToInt(e -> e.intValue()).toArray();
// int[] resArray = new int[resList.size()];
// for (int i = 0; i < resList.size(); i++)
// resArray[i] = resList.get(i);
// return resArray;
}
}
// go
func levelOrder(root *TreeNode) []int {
if root == nil {
return []int{
}
}
var res = make([]int, 0)
queue := make([]*TreeNode, 0)
queue = append(queue, root)
for len(queue) != 0 {
node := queue[0]
queue = queue[1:] // 模拟队列出队
res = append(res, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
return res
}
15. 从上到下打印二叉树 II
题目:剑指 Offer 32 - II. 从上到下打印二叉树 II
解法 1:队列
题解:面试题32 - II. 从上到下打印二叉树 II(层序遍历 BFS,清晰图解)
class Solution {
public List<List<Integer>> levelOrder1(TreeNode root) {
if (root == null) return new ArrayList<>();
List<List<Integer>> resList = new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>() {
{
offer(root); }};
while (!queue.isEmpty()) {
List<Integer> tmpList = new ArrayList<>();
// 取出一行元素
for (int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll();
tmpList.add(node.val);
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
}
resList.add(tmpList);
}
return resList;
}
}
解法 2:递归
这个解法是比较妙的,本质上是个先序遍历,
- 遍历到哪一层就将这一层的 list 创建好,填入第一个元素
- 当后面再次遍历来到这一层时,利用二维 list 的下标将结果放到对应的层中
class Solution {
List<List<Integer>> resList = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
helper(root, 0);
return resList;
}
// 本质上是先序遍历
public void helper(TreeNode node, int level) {
if (node == null) return;
if (resList.size() <= level)
resList.add(new ArrayList<>());
resList.get(level).add(node.val);
helper(node.left, level + 1);
helper(node.right, level + 1);
}
}
16. 从上到下打印二叉树 III
题目:剑指 Offer 32 - III. 从上到下打印二叉树 III
_解法 1:双栈
思路:二叉树层次遍历的大模板下,加一些额外操作而已。
// java
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>();
List<List<Integer>> resList = new ArrayList<>();
Stack<TreeNode> stack1 = new Stack<>();
Stack<TreeNode> stack2 = new Stack<>();
stack1.add(root);
boolean flag = true;
while (!(stack1.isEmpty() && stack2.isEmpty())) {
List<Integer> tmpList = new ArrayList<>();
if (flag) {
for (int i = stack1.size(); i > 0; i--) {
TreeNode node = stack1.pop();
tmpList.add(node.val);
if (node.left != null)
stack2.add(node.left);
if (node.right != null)
stack2.add(node.right);
}
} else {
for (int i = stack2.size(); i > 0; i--) {
TreeNode node = stack2.pop();
tmpList.add(node.val);
if (node.right != null)
stack1.add(node.right);
if (node.left != null)
stack1.add(node.left);
}
}
resList.add(tmpList);
flag = !flag;
}
return resList;
}
解法 2:双端队列
题解:面试题32 - III. 从上到下打印二叉树 III(层序遍历 BFS / 双端队列,清晰图解
// java
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>();
List<List<Integer>> resList = new ArrayList<>();
Deque<TreeNode> deque = new LinkedList<>();
deque.add(root);
while (!deque.isEmpty()) {
List<Integer> tmpList = new ArrayList<>();
// 打印奇数层
for (int i = deque.size(); i > 0; i--) {
// 左 -> 右
TreeNode node = deque.removeFirst();
tmpList.add(node.val);
// 先左后右加入下层节点
if (node.left != null)
deque.addLast(node.left);
if (node.right != null)
deque.addLast(node.right);
}
resList.add(tmpList);
if (deque.isEmpty()) break;
// 打印偶数层
tmpList = new ArrayList<>();
for (int i = deque.size(); i > 0; i--) {
// 右 -> 左
TreeNode node = deque.removeLast();
tmpList.add(node.val);
// 先右后左加入下层节点
if (node.right != null)
deque.addFirst(node.right);
if (node.left != null)
deque.addFirst(node.left);
}
resList.add(tmpList);
}
return resList;
}
解法 3:层序遍历 + 倒序
思路:在层序遍历的基础上,添加时将奇数层倒序。
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) return new ArrayList<>(); List<List<Integer>> resList = new ArrayList<>(); Queue<TreeNode> queue = new LinkedList<>(); queue.add(root); while (!queue.isEmpty()) {
List<Integer> tmpList = new ArrayList<>(); for (int i = queue.size(); i > 0; i--) {
TreeNode node = queue.poll(); tmpList.add(node.val); if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } // 奇数层倒序 if (resList.size() % 2== 1) Collections.reverse(tmpList); resList.add(tmpList); } return resList;}
Go 语言 切片
Go 语言模拟队列的先进先出,相当于取出头部一个值后,丢弃这个值
queue = queue[1:]
func levelOrder(root *TreeNode) [][]int {
var res = make([][]int, 0)
if root == nil {
return res
}
queue := []*TreeNode{
root}
flag := false // 奇偶顺序控制
for len(queue) > 0 {
length := len(queue)
tmp := make([]int, length)
for i := 0; i < length; i++ {
node := queue[0]
queue = queue[1:]
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
if flag {
tmp[length-i-1] = node.Val
} else {
tmp[i] = node.Val
}
}
res = append(res, tmp)
flag = !flag
}
return res
}
17. 树的子结构
解法 1:双重递归 DFS
我的 helper 函数基本写对了,不过 isSubStructure 没考虑到用递归。。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (A == null || B == null) return false;
return helper(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
boolean helper(TreeNode A, TreeNode B) {
if (B == null) return true;
if (A == null || A.val != B.val) return false;
return helper(A.left, B.left) && helper(A.right, B.right);
}
}
解法 2:双重 BFS(很慢)
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (A == null || B == null) return false;
Queue<TreeNode> queue = new LinkedList<>() {
{
offer(A); }};
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
if (node.val == B.val)
if (helper(node, B))
return true;
if (node.left != null)
queue.offer(node.left);
if (node.right != null)
queue.offer(node.right);
}
return false;
}
boolean helper(TreeNode A, TreeNode B) {
Queue<TreeNode> queueA = new LinkedList<>() {
{
offer(A); }};
Queue<TreeNode> queueB = new LinkedList<>() {
{
offer(B); }};
while (!queueB.isEmpty()) {
TreeNode nodeA = queueA.poll();
TreeNode nodeB = queueB.poll();
if (nodeA == null || nodeA.val != nodeB.val)
return false;
if (nodeB.left != null) {
queueA.offer(nodeA.left);
queueB.offer(nodeB.left);
}
if (nodeB.right != null) {
queueA.offer(nodeA.right);
queueB.offer(nodeB.right);
}
}
return true;
}
}
18.二叉树的镜像
_解法 1:递归 DFS
这题很简单!很适合用来检验对递归的掌握程度。
// java
public TreeNode mirrorTree(TreeNode root) {
if (root == null) return null;
TreeNode leftNode = mirrorTree(root.left);
TreeNode rightNode = mirrorTree(root.right);
root.left = rightNode;
root.right = leftNode;
return root;
}
利用 Go 语言平行赋值的写法,可以省略暂存操作。
// go
func mirrorTree(root *TreeNode) *TreeNode {
if root == nil {
return nil
}
leftNode, rightNode := mirrorTree(root.Left), mirrorTree(root.Right)
root.Left, root.Right = rightNode, leftNode
return root
}
解法 2:辅助栈
题解:剑指 Offer 27. 二叉树的镜像(递归 / 辅助栈,清晰图解)
// java
public TreeNode mirrorTree1(TreeNode root) {
if (root == null) return null;
Stack<TreeNode> stack = new Stack<>() {
{
add(root); }};
while (!stack.empty()) {
TreeNode node = stack.pop();
if (node.left != null)
stack.add(node.left);
if (node.right != null)
stack.add(node.right);
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
}
return root;
}
19. 对称的二叉树
_解法 1:暴力
注意,这里写的获取二叉树的镜像涉及到一些 Java 值传递的概念,不可以在 root 上直接修改,需要创建一个新的对象,或者 root = new TreeNode(root.val)
也是可以的。
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
TreeNode newTree = mirrorTree(root);
return isSameTree(root, newTree);
}
/**
* 判断两棵二叉树是否相同
*/
public boolean isSameTree(TreeNode p, TreeNode q) {
if (p == null && q == null) return true;
if ((p != null && q ==null) || (p == null && q != null))
return false;
if (p.val != q.val)
return false;
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
/**
* 获得二叉树的镜像
*/
TreeNode mirrorTree(TreeNode root) {
if (root == null) return null;
TreeNode leftNode = mirrorTree(root.left);
TreeNode rightNode = mirrorTree(root.right);
TreeNode node = new TreeNode(root.val);
node.left = rightNode;
node.right = leftNode;
return node;
}
}
解法 2:递归
1、递归的函数要干什么?
- 函数的作用是判断传入的两个树是否镜像。
- 输入:TreeNode left, TreeNode right
- 输出:是:true,不是:false
2、递归停止的条件是什么?
- 左节点和右节点都为空 -> 倒底了都长得一样 ->true
- 左节点为空的时候右节点不为空,或反之 -> 长得不一样-> false
- 左右节点值不相等 -> 长得不一样 -> false
3、从某层到下一层的关系是什么?
- 要想两棵树镜像,那么一棵树左边的左边要和二棵树右边的右边镜像,一棵树左边的右边要和二棵树右边的左边镜像
- 调用递归函数传入左左和右右
- 调用递归函数传入左右和右左
- 只有左左和右右镜像且左右和右左镜像的时候,我们才能说这两棵树是镜像的
4、调用递归函数
- 我们想知道它的左右孩子是否镜像,传入的值是 root 的左孩子和右孩子。
- 这之前记得判个 root==null。
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root ==null) return true;
return isMirror(root.left, root.right);
}
/**
* 判断传入的两颗树是否镜像
*/
boolean isMirror(TreeNode left, TreeNode right) {
if (left == null && right == null) return true;
if (left == null || right == null) return false;
if (left.val != right.val) return false;
return isMirror(left.left, right.right) && isMirror(left.right, right.left);
}
}