《剑指 Offer I》刷题笔记 1 ~10 题

小标题以 _ 开头的题目和解法代表独立想到思路及编码完成,其他是对题解的学习。

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 := 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:递归

参考题解:面试题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 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:字符串切片

题解:面试题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 := 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}

猜你喜欢

转载自blog.csdn.net/weixin_43734095/article/details/123162452