剑指offer刷题

目录

文章目录

查漏补缺

空间复杂度:

//程序调用函数是基于栈实现的
// 1.空间复杂度为O(1)
int test() {
    return 0;
}

void algorithm(int N) {
    for (int i = 0; i < N; i++) {
        test();
    }
}
// 2.空间复杂度为O(N)
int algorithm(int N) {
    if (N <= 1) return 1;
    return algorithm(N - 1) + 1;
}

 

LinkedList

LinkedList的存储原理存储原理是一个链表,在元素的前后分别有一个前置结点和后置结点,用于连接集合中的上一个元素和下一个元素,依次“手拉手”,构成一条链式数据的集合。
//方法    
LinkedList.removeLast()方法用于从LinkedList中删除最后一个元素。删除元素后,此方法还返回元素。
 LinkedList 类的 addFirst()addLast() 方法在链表的开头和结尾添加元素 
peek() 查看顶端元素,无返回null
push 等价于 addFirst
pop 等价于 removeFirst
peek 为首个元素,Stack 类的peek方法,peek是栈顶,即对应链表的最后一个元素    

stack

isEmpty(),pop()返回栈顶元素,并在进程中删除它。
push(),peek()查看栈顶元素
search 返回对象在堆栈中的位置,以 1 为基数。

Object类型

Object类型,我们无论如何都用equals比较就对了。Integer  需要使用 equals() 代替 == ,以比较对象的值。

java stack中add 和 push的区别

Stack的add方法是从Vector继承来的,add返回布尔类型 而push则返回插入元素的类型。

java中String、StringBuffer和StringBuilder的区别(简单介绍)

三者共同之处:都是final类,不允许被继承
不同:
	速度:StringBuilder > StringBuffer > String
 		 String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的
 	在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
 	
总结:
 	String:适用于少量的字符串操作的情况
   StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
   StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
方法
	

Collections.reverse对list集合 翻转

由数据范围反推算法复杂度以及算法内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-03xQtFVl-1654313778004)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220510150104782.png)]

一、数据结构

1.栈与队列
剑指 Offer 09. 用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

/* 思路
   双栈法:
   栈instack入栈,栈outStack 倒序出栈
 */
public class CQueue {
    LinkedList<Integer> inStack,outStack;
    public CQueue() {
        inStack = new LinkedList<Integer>();
        outStack = new LinkedList<Integer>();
    }

    public void appendTail(int value) {
        inStack.add(value);
    }

    public int deleteHead() {
        // 都为空,则无元素,返回-1
        if(inStack.isEmpty() && outStack.isEmpty()) return -1;
        // outstack非空,则出栈
        if(!outStack.isEmpty()) return outStack.removeLast();
        //否则,instack元素 移到outstack,最后出栈
        while(!inStack.isEmpty()){
            outStack.addLast(inStack.removeLast());
        }
        return outStack.removeLast();
    }
}
// 用Stack 类
class CQueue {
    Stack<Integer> inStack,outStack;
    public CQueue() {
        inStack = new Stack<Integer>();
        outStack = new Stack<Integer>();
    }

    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();

    }
}
剑指 Offer 30. 包含min函数的栈

定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

/*
   双栈法:
        栈A 存储所有元素,保证push()、pop()、top()
        栈B 副组栈,存储栈 A 中所有 非严格降序 元素的子序列,保证栈B的栈顶是栈A的最小元素

 */
public class MinStack {
        Stack<Integer> A,B;

        /** initialize your data structure here. */
        public MinStack() {
            A = new Stack<>();
            B = new Stack<>();
        }

        public void push(int x) {
            A.push(x);
            // 如何B空 或 加入元素小于B
            if(B.isEmpty() || x <= B.peek())
                B.push(x);
        }
		// A的栈顶 是A的最小值则B也出栈
        public void pop() {
            if(A.pop().equals(B.peek()))
                B.pop();
        }

        public int top() {
            return A.peek();
        }

        public int min() {
            return B.peek();
        }
    }

剑指 Offer 59 - I. 滑动窗口的最大值

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

示例:

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]

法1:暴力 循环 O(nk)

 public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums.length== 0) return new int[0];
        int[] res = new int[nums.length - k + 1];
        for(int i=0;i<nums.length- k + 1;i++){
            res[i] = nums[i];// res[i] 初始化
             for(int j=i+1;j<i+k;j++)
                if(res[i] < nums[j])
                    res[i] = nums[j];            
        }
    return res;
}

法2:双端队列:同offer30 包含min函数的栈,队列存放 非严格递减的数字

public int[] maxSlidingWindow(int[] nums, int k) {
    if(nums.length == 0) return new int[0];
    Deque<Integer> deque = new LinkedList<>();
    int[] res  = new int[nums.length - k +1];
    // 未形成窗口时:形成非严格递减的数组,i:左边界,j:有边界
    for(int i=0; i< k;i++){
        // 非空,并且新加入的值大于 队列末尾值,说明末尾值不可能为 最大值,可以删掉
        while(!deque.isEmpty() && nums[i] > deque.peekLast())
            deque.removeLast();
        deque.addLast(nums[i]);
    }
    res[0] = deque.peekFirst();
    //形成窗口时
    for(int i = k; i < nums.length ; i++){
        if(deque.peek() == nums[i-k]) deque.removeFirst();
        while(!deque.isEmpty() && deque.peekLast() < nums[i])// 注:deque 要非空,上一步可以把deque删空
            deque.removeLast();
        deque.addLast(nums[i]);
        res[i-k+1] = deque.peekFirst();
    }
    return res;
}
剑指 Offer 59 - II. 队列的最大值

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入:
[“MaxQueue”,“push_back”,“push_back”,“max_value”,“pop_front”,“max_value”]
[[],[1],[2],[],[],[]]
输出: [null,null,null,2,1,2]

// 用2个队列,第二个队列形成严格非递减 数列,同上一题
 class MaxQueue {
     Queue<Integer> A;
     Deque<Integer> B;
     public MaxQueue() {
         A = new LinkedList<Integer>();
         B = new LinkedList<Integer>();
     }

     public int max_value() {
         return B.isEmpty() ? -1 : B.peekFirst();
     }

     public void push_back(int value) {
         A.offer(value);
         while (!B.isEmpty() && value > B.peekLast())
             B.removeLast();
         B.offerLast(value);

     }

     public int pop_front() {
         if(A.isEmpty()) return -1;
         if(A.peek().equals(B.peekFirst()))
             B.removeFirst();
         return A.poll();
     }
 }
2.链表
剑指 Offer 06. 从尾到头打印链表

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。

class ListNode {
 int val;
 ListNode next;
 ListNode(int x) { val = x; }
}

法一:辅助账

/*
    辅助栈法:逆序,栈:先进后出
    时间复杂度:O(N),遍历链表入栈,遍历栈出栈
    空间复杂度:O(N) 辅助栈O(N),数组arr O(N)
 */
class Solution {
    public int[] reversePrint(ListNode head) {
        Stack<Integer> stack = new Stack<Integer>();
        while(head != null){
            stack.push(head.val);
            head=head.next;
        }
        // 栈转数组
        int[] arr = new int[stack.size()];
        for(int i=0;i<arr.length; i++)
            arr[i] = stack.pop();
        return arr;
        
    }
}

法二:递归法

/*
	递归法 逆序输出
	时间复杂度:O(N),遍历链表,递归 N 次。
    空间复杂度:O(N) 系统递归需要使用 O(N) 的栈空间。
*/
class  Solution{
    ArrayList<Integer> arrayList = new ArrayList<Integer>();
    public int[] reversePrint(ListNode head) {
        // 递归,将链表逆序存放到 可变数组arrayList中
       recur(head);
       // 动态数组 转 静态数组
        int[] arr = new int[arrayList.size()];
        for(int i=0;i<arr.length; i++)
            arr[i] = arrayList.get(i);
        return arr;
    }

    void recur(ListNode head) {
        // 1.终止条件 2.递推 3.回溯
        if(head == null) return;
        recur(head.next);
        arr.add(head.val);
    }

}
剑指 Offer 24. 反转链表

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

法1:头插法 加一个头节点原地翻转链表

public ListNode reverseList(ListNode head) {
        ListNode L = new ListNode(0);
        L.next = head;

        ListNode p = L.next,q;
        L.next = null;
        while(p != null){
            q = p.next;
            p.next = L.next;
            L.next = p;
            p = q;
        }
        return L.next;
    }

法2:双指针迭代,遍历链表 并修改节点引用指向

//pre 前一个节点 cur表当前节点
public ListNode reverseList(ListNode head) {
    ListNode cur = head, pre = null;
    while(cur != null) {
        ListNode tmp = cur.next; // 暂存后继节点 cur.next
        cur.next = pre;          // 修改 next 引用指向
        pre = cur;               // pre 暂存 cur
        cur = tmp;               // cur 访问下一节点
    }
    return pre;
}

法3:递归

public ListNode reverseList(ListNode head) {
    return recur(head, null);    // 调用递归并返回
}
private ListNode recur(ListNode cur, ListNode pre) {
    if (cur == null) return pre; // 终止条件
    ListNode res = recur(cur.next, cur);  // 递归后继节点
    cur.next = pre;              // 修改节点引用指向
    return res;                  // 返回反转链表的头节点
}
剑指 Offer 35. 复杂链表的复制

请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pq7dr0v4-1654313778007)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220515170528581.png)]

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/
public Node copyRandomList(Node head) {
    if(head == null) return null;
    Node cur = head;
    Map<Node,Node> map = new HashMap<>();
    // 复制各节点,形成 键值== 旧节点:新节点
    while(cur != null){
        map.put(cur,new Node(cur.val));
        cur = cur.next;
    }
    cur = head;
    //构建新链表的 next 和 random 指向
    while(cur != null){
        // get获取map的值,即新节点
        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 05. 替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

法一:内置函数

return s.replaceAll(" ","%20");

法二:遍历法 :

 public String replaceSpace(String s) {
        String tmp="";
        for (int i = 0; i < s.length(); i++) {
            if(s.charAt(i) ==' ') tmp += "%20";
            else tmp += s.charAt(i);
        }
        s = tmp;
        return s;
    }
// 改进,将辅助数组tmp由String 类型改成 StringBuilder类型
public String replaceSpace(String s) {
        StringBuilder tmp = new StringBuilder();
        for(Character c : s.toCharArray())
        {
            if(c == ' ') tmp.append("%20");
            else tmp.append(c);
        }
        return tmp.toString();
    }

补充:java中String、StringBuffer和StringBuilder的区别(简单介绍)

三者共同之处:都是final类,不允许被继承
不同:
	速度:StringBuilder > StringBuffer > String
 		 String最慢的原因:String为字符串常量,而StringBuilder和StringBuffer均为字符串变量,即String对象一旦创建之后该对象是不可更改的,但后两者的对象是变量,是可以更改的
 	在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
 	 
剑指 Offer 58 - II. 左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

法1:字符串切片

return s.substring(n, s.length()) + s.substring(0, n);
substring左闭右开

法2:辅助字符串,列表遍历拼接

public String reverseLeftWords(String s, int n) {
    StringBuffer tmp = new StringBuffer();
    for(int i = n; i < n + s.length(); i++)
        tmp.append(s.charAt(i % s.length()));
    return tmp.toString();
}

法3:字符串拼接(效率低下,String 不可变)

??? 剑指 Offer 20. 表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。

数值(按顺序)可以分成以下几个部分:

若干空格
一个 小数 或者 整数
(可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个 整数
若干空格

小数(按顺序)可以分成以下几个部分:

(可选)一个符号字符(‘+’ 或 ‘-’)
下述格式之一:
至少一位数字,后面跟着一个点 ‘.’
至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
一个点 ‘.’ ,后面跟着至少一位数字

整数(按顺序)可以分成以下几个部分:

(可选)一个符号字符(‘+’ 或 ‘-’)
至少一位数字

部分数值列举如下:

[“+100”, “5e2”, “-123”, “3.1416”, “-1E-16”, “0123”]

部分非数值列举如下:

[“12e”, “1a3.14”, “1.2.3”, “±5”, “12e+5.4”]

示例 1:

输入:s = “0”
输出:true

示例 2:

输入:s = “e”
输出:false

示例 3:

输入:s = “.”
输出:false

示例 4:

输入:s = " .1 "
输出:true

tips: 本题使用有限状态自动机。根据字符类型和合法数值的特点,先定义状态,再画出状态转移图,最后编写代码即可。

class Solution {
    public boolean isNumber(String s) {
        Map[] states = {
            new HashMap<>() {{ put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }}, // 0.
            new HashMap<>() {{ put('d', 2); put('.', 4); }},                           // 1.
            new HashMap<>() {{ put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }}, // 2.
            new HashMap<>() {{ put('d', 3); put('e', 5); put(' ', 8); }},              // 3.
            new HashMap<>() {{ put('d', 3); }},                                        // 4.
            new HashMap<>() {{ put('s', 6); put('d', 7); }},                           // 5.
            new HashMap<>() {{ put('d', 7); }},                                        // 6.
            new HashMap<>() {{ put('d', 7); put(' ', 8); }},                           // 7.
            new HashMap<>() {{ put(' ', 8); }}                                         // 8.
        };
        int p = 0;
        char t;
        for(char c : s.toCharArray()) {
            if(c >= '0' && c <= '9') t = 'd';
            else if(c == '+' || c == '-') t = 's';
            else if(c == 'e' || c == 'E') t = 'e';
            else if(c == '.' || c == ' ') t = c;
            else t = '?';
            if(!states[p].containsKey(t)) return false;
            p = (int)states[p].get(t);
        }
        return p == 2 || p == 3 || p == 7 || p == 8;
    }
}

剑指 Offer 67. 把字符串转换成整数

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

说明:

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。

示例 1:

输入: “42”
输出: 42

示例 2:

输入: " -42"
输出: -42
解释: 第一个非空白字符为 ‘-’, 它是一个负号。
我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。

示例 3:

输入: “4193 with words”
输出: 4193
解释: 转换截止于数字 ‘3’ ,因为它的下一个字符不为数字。

示例 4:

输入: “words and 987”
输出: 0
解释: 第一个非空字符是 ‘w’, 但它不是数字或正、负号。
因此无法执行有效的转换。

示例 5:

输入: “-91283472332”
输出: -2147483648
解释: 数字 “-91283472332” 超过 32 位有符号整数范围。
因此返回 INT_MIN (−231) 。

//注:边界问题
// 最大值:2147483647
//最小值:-2147483648
public int strToInt(String str) {
    int res = 0, bndry = Integer.MAX_VALUE / 10;
    int i = 0, sign = 1, length = str.length();//sing为1是表正数,为-1 表负数
    if(length == 0) return 0;
    while(str.charAt(i) == ' ')
        if(++i == length) return 0; //判断是否是 " "
    // 判断 +,- 符号
    if(str.charAt(i) == '-') sign = -1;
    if(str.charAt(i) == '-' || str.charAt(i) == '+') i++;


    for(int j = i; j < length; j++) {
        if(str.charAt(j) < '0' || str.charAt(j) > '9') break;
        // 判断越界 2 147 483 647 ,bndry = 2 147 483 64
        // ① 拼接后大于等于xx483 65 *10
        // ② 拼接后大于等于xx483 647
        if(res > bndry || res == bndry && str.charAt(j) > '7')
            return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
        res = res * 10 + (str.charAt(j) - '0');
    }
    return sign * res;
}

二、查找算法

剑指 Offer 03. 数组中重复的数字

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

法1:排序法 + 遍历

// 排序法 + 遍历
public int findRepeatNumber(int[] nums) {
        Arrays.sort(nums);
        for(int i = 0;i<nums.length - 1; i++)
            if(nums[i] == nums[i + 1])
                return nums[i];
        return -1;
}

法2:哈希

利用数据结构特点,容易想到使用哈希表(Set)记录数组的各个数字,当查找到重复数字则直接返回。

时间复杂度 O(N) : 遍历数组使用 O(N)O(N) ,HashSet 添加与查找元素皆为 O(1) 。
空间复杂度 O(N) : HashSet 占用O(N) 大小的额外空间。

  1. 新建哈希表
  2. 遍历数组nums,数组中数字在哈希表中重复则返回,否则添加进哈希表
  3. 无重复数字,返回-1
public int findRepeatNumber(int[] nums) {
        Set<Integer> dic = new HashSet<>();
        for(int num:nums){
            if(dic.contains(num)) return num;
            dic.add(num);
        }
        return -1;
    }

法3:辅助数组打卡标记

共有n个数字,每个数字都在 0~n-1 范围内

// 时空复杂度:O(n)、O(n)
public int duplicateInArray(int[] nums) {
    // 建立新数组标记数组中 数字出现的次数  
    int[] temp = new int[nums.length];
    for(int i=0;i<nums.length;i++)
        temp[nums[i]]++;
        
    for(int i=0;i<temp.length;i++)
        if(temp[i] >1 ) //次数大于1 就是重复的
            return i;//返回的数字i
    return -1;

}

解法4:原地交换,移动元素让下标和值相对应

1.遍历数组 nums,设索引初始值为 i = 0;

  1. 若 nums[i] = i 说明此数字已在对应索引位置,无需交换,因此跳过;
  2. 若 nums[nums[i]] = nums[i] 代表索引  `nums[i]`处和索引 `i `处的元素值都为 nums[i] ,即找到一组重复值,返回此值 nums[i]
  3.否则: 交换`索引`为 `i `和 `nums[i] `的元素值,将此数字交换至对应索引位置。
  1. 若遍历完毕尚未返回,则返回 -1 。
public int findRepeatNumber(int[] nums) {
    for(int i = 0;i < nums.length; ){
        if(i == nums[i]) {
            i++;
            continue;
        }
        // 否则,交换2个数  nums[5] 与 nums[nums[1]
        if(nums[i] == nums[nums[i]]) return nums[i];
        //i=0,nums[0] = 5 与 nums[5] 进行交换
        int tmp = nums[i];//tmp = nums[0] = 5
        nums[i] = nums[nums[i]];// nums[0] 与nums[5] 交换
        nums[tmp] = tmp;
    }
    return -1;
}
剑指 Offer 53 - I. 在排序数组中查找数字 I

统计一个数字在排序数组中出现的次数。

示例 1:

输入: nums = [5,7,7,8,8,10], target = 8
输出: 2

法1:暴力

 public int search(int[] nums, int target) {
        int cnt = 0;
        for(int i = 0; i < nums.length; i++)
            if(nums[i] ==target)
                cnt++;
        return  cnt;
    }

法2:双指针

public int search(int[] nums, int target) {
    // i从前往后遍历,j从后往前遍历
    int i=0,j=nums.length-1;
    while(i<j){
        if(nums[i]<target) i++;//左边界
        if(nums[j] > target) j--;//有边界
        if(nums[j]== target && nums[i]==target) break;
    }
    // 处理特殊情况
    if(i==j) {
        if(nums[j]== target) return 1;
        else return 0;
    }
    return j-i+1;
}

法3:二分法

排序数组中的搜索问题,首先想到 二分法 解决。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JE4n05uj-1654313778008)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220415081732250.png)]

???

剑指 Offer 53 - II. 0~n-1中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

示例 1:

输入: [0,1,3]
输出: 2

法1:暴力

public int missingNumber(int[] nums) {
       for(int i = 0; i < nums.length; i ++) {
            if(nums[i] != i) 
                return i;
      		return nums.length;
    	}
}

法2:二分法

排序数组中的搜索问题,首先想到 二分法 解决

数组可以分为

  • 左边 nums[i] == i
  • 右边 nums[i] != i

1.初始化 2.循环二分 3.返回值

public int missingNumber(int[] nums) {
        int i = 0, j = nums.length - 1;
        while(i <= j) {
            int m = (i + j) / 2;
            if(nums[m] == m) i = m + 1;
            else j = m - 1;
        }
        return i;
    }

法3:数学法

 public int missingNumber(int[] nums) {
        // 等差数列 求和 0~n
        int sum = (0 + nums.length) * (nums.length + 1)/2;
        for(int i = 0; i < nums.length; i++)
            sum -= nums[i];
        return sum;
    }
剑指 Offer 04. 二维数组中的查找

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。

给定 target = 20,返回 false。


剑指 Offer 11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。

给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2][1,2,3,4,5] 的一次旋转,该数组的最小值为 1。

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]


剑指 Offer 50. 第一个只出现一次的字符

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例 1:

输入:s = “abaccdeff”
输出:‘b’


三、搜索与回属算法

剑指 Offer 32 - I. 从上到下打印二叉树

从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。

例如:
给定二叉树: [3,9,20,null,null,15,7],

3

/
9 20
/
15 7
返回:

[3,9,20,15,7]

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
/*
	即按层打印,BFS 广度优先搜索,抽象数据模型:队列
*/
public int[] reverseList(TreeNode root) {
        // 空树,返回0
        if (root == null) return new int[0];
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        ArrayList<Integer> arr = new ArrayList<Integer>();
        // 将树的根节点 数值加入数组中,左右节点加入队列中
        while (!queue.isEmpty()) {
            // 拿到队列的队首元素,并进行删除
            TreeNode node = queue.poll();
            arr.add(node.val);
            if (node.left != null) queue.offer(node.left);
            if (node.right != null) queue.offer(node.right);
        }
       //将动态数组转换为静态数组
        int[] res = new int[arr.size()];
        for (int i = 0; i < arr.size(); i++)
            res[i] = arr.get(i);
        return res;

    }
剑指 Offer 32 - II. 从上到下打印二叉树 II

从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行

例如:
给定二叉树: [3,9,20,null,null,15,7],

3

/
9 20
/
15 7
返回其层次遍历结果:

[
[3],
[9,20],
[15,7]
]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9BSS0kVL-1654313778008)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220510084815936.png)]

//与之前那道题相比,把每一层的节点值 放入到了一个临时List中
public List<List<Integer>> levelOrder(TreeNode root) {
    if(root == null) return new ArrayList<>();
    List<List<Integer>> arr = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();

    queue.add(root);
    while(!queue.isEmpty()){
        // 把每一层的节点值 放入tmpList中
        List<Integer> tmpList = new ArrayList<>();
        // 不能i 从0 开始,因为在循环中queue 的sieze()大小会发生改变
        for(int i = queue.size();i>0; i--){
            TreeNode tmpNode = queue.poll();
            tmpList.add(tmpNode.val);
            if(tmpNode.left != null) queue.add(tmpNode.left);
            if(tmpNode.right != null) queue.add(tmpNode.right);
        }
        arr.add(tmpList);
    }
    return arr;

}
剑指 Offer 32 - III. 从上到下打印二叉树 III

请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推

例如:
给定二叉树: [3,9,20,null,null,15,7],

​ 3

/
9 20
/
15 7
返回其层次遍历结果:

[
[3],
[20,9],
[15,7]
]

提示:

节点总数 <= 1000

法1: 层序遍历 + 双端队列

public List<List<Integer>> levelOrder(TreeNode root) {
    if(root == null) return new ArrayList<>();
    List<List<Integer>> arr = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();

    queue.add(root);
    while(!queue.isEmpty()){
        // 把每一层的节点值 放入tmpList中
        // 把List 改成LinkedList 双端队列
        //List<Integer> tmpList = new ArrayList<>();
        LinkedList<Integer> tmpList = new LinkedList<>();
        // 不能i 从0 开始,因为在循环中queue 的sieze()大小会发生改变
        for(int i = queue.size();i>0; i--){
            TreeNode tmpNode = queue.poll();
            // arr 的奇数层 按顺序添加头部,偶数层 按逆序依次添加尾部
            if(arr.size() % 2 == 1) tmpList.addFirst(tmpNode.val);
            else tmpList.addLast(tmpNode.val);

            if(tmpNode.left != null) queue.add(tmpNode.left);
            if(tmpNode.right != null) queue.add(tmpNode.right);
        }
        arr.add(tmpList);
    }
    return arr;
}

法2:层序遍历 + 倒序

//相比题2,只多了一行 倒序
public List<List<Integer>> levelOrder(TreeNode root) {
    if(root == null) return new ArrayList<>();
    List<List<Integer>> arr = new ArrayList<>();
    Queue<TreeNode> queue = new LinkedList<>();

    queue.add(root);
    while(!queue.isEmpty()){
        // 把每一层的节点值 放入tmpList中
        List<Integer> tmpList = new ArrayList<>();
        // 不能i 从0 开始,因为在循环中queue 的sieze()大小会发生改变
        for(int i = queue.size();i>0; i--){
            TreeNode tmpNode = queue.poll();
            tmpList.add(tmpNode.val);
            if(tmpNode.left != null) queue.add(tmpNode.left);
            if(tmpNode.right != null) queue.add(tmpNode.right);
        }
        //arr.size() + 1 是偶数表偶数层,即arr.size() 奇数,翻转 tmpList
        if(arr.size() % 2 == 1) Collections.reverse(tmpList);
        arr.add(tmpList);
    }
    return arr;
}
剑指 Offer 26. 树的子结构

输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)

B是A的子结构, 即 A中有出现和B相同的结构和节点值。

例如:
给定的树 A:

​ 3
​ / \

4 5
/
1 2
给定的树 B:

4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。

示例 1:

输入:A = [1,2,3], B = [3,1]
输出:false
示例 2:

输入:A = [3,4,5,1,2], B = [4,1]
输出:true
限制:

0 <= 节点个数 <= 10000

//先序遍历
public boolean isSubStructure(TreeNode A, TreeNode B) {
    if(B==null || A==null) return false;
    // 先序遍历 判断B是否是 A的子结构
    if(recur(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B))
        return true;
    return false;
   // 简略写法
   //return (A != null && B != null) && (recur(A, B) || isSubStructure(A.left, B) || isSubStructure(A.right, B));
}

boolean recur(TreeNode A, TreeNode B) {
    //B为空说明B树已经匹配完了
    if (B == null) return true;
    //A为空说明A匹配完了
    if (A == null || A.val != B.val) return false;
    return recur(A.left, B.left) && recur(A.right, B.right);
}
剑指 Offer 27. 二叉树的镜像

请完成一个函数,输入一个二叉树,该函数输出它的镜像。

例如输入:

4

/
2 7
/ \ /
1 3 6 9
镜像输出:

​ 4

/
7 2
/ \ /
9 6 3 1

示例 1:

输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]

法1:递归交换左右子树

public TreeNode mirrorTree(TreeNode root) {
    if(root == null) return null;
    //交换左右子树
    TreeNode tmpNode = root.left;
    root.left = root.right;
    root.right = tmpNode;
    // 递归左右子树
    mirrorTree(root.left);
    mirrorTree(root.right);
    return root;
}
剑指 Offer 28. 对称的二叉树

请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。

​ 1

/
2 2
/ \ /
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:

​ 1

/
2 2
\
3 3

示例 1:

输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:

输入:root = [1,2,2,null,3,null,3]
输出:false

/*	要对称
	L.val = R.val :即此两对称节点值相等。
	L.left.val = R.right.val :即 LL 的 左子节点 和 RR 的 右子节点 对称;
	L.right.val = R.left.val :即 LL 的 右子节点 和 RR 的 左子节点 对称。
*/
public boolean isSymmetric(TreeNode root) {
    return root == null || recur(root.left, root.right);
}

boolean recur(TreeNode L, TreeNode R) {
    if(L == null && R == null) return true;
    if(L == null || R == null || L.val != R.val) return false;
    return recur(L.left, R.right) && recur(L.right, R.left);
}

剑指 Offer 12. 矩阵中的路径

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例 1:

输入:board = [[“A",“B”,"C”,“E”],

​ [“S”,“F”,“C”,“S”],

​ [“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
示例 2:

输入:board = [[“a”,“b”],

​ [“c”,“d”]], word = “abcd”
输出:false

dfs怎么写

​ 1)遍历每个起点
​ 2)进了dfs首先判断合不合理,不合理就是false (剪枝)
​ 3)判断是否到了终点,是就返回true
​ 4)没有到终点,继续深入dfs, dfs的时候按照给定的方向,一般有两种pattern
​ (一是向四方向扩散,而是向八方向扩散)
​ 深入回来以后,如果是true直接返回上一层true,否则继续扩散
​ 5)扩散完还没解,那就返回false,让上一层向其他方向扩散,开始新的dfs

深度优先搜索(DFS)+ 剪枝

 // board 二维数组,word 目标字符串
public boolean exist(char[][] board, String word) {
    char[] words = word.toCharArray();
    // 1)遍历每个起点
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board[0].length; j++) {
            if (dfs(board, words, i, j, 0)) return true;
        }
    }
    // 全部遍历完了 没有符合题目的情况
    return false;
}


// i 表示行,j表示列,k表示遍历word的索引
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
    // 2)进了dfs首先判断合不合理,不合理就是false   ① 上下左右 越界 ② 与目标字符 不匹配
    if (i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]) return false;
    // 3)判断是否到了终点,是就返回true
    if (k == word.length - 1) return true;
    //4)没有到终点,继续深入dfs, dfs的时候按照给定的方向,一般有两种pattern
    //    (一是向四方向扩散,而是向八方向扩散)
    //    深入回来以后,如果是true直接返回上一层true,否则继续扩散
    // 5)扩散完还没解,那就返回false,让上一层向其他方向扩散,开始新的dfs

    board[i][j] = '\0'; // 表示该字符 已经被访问了
    boolean res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i - 1, j, k + 1) 
        || dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i, j - 1, k + 1);
    
    board[i][j] = word[k]; // 回溯
    return res;
}
剑指 Offer 13. 机器人的运动范围

上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3

012
0truetruefalse
1true1 + 1 >2 所以不行false

法1:dfs

class Solution {
   int m, n, k;
    boolean[][] visited;// 表示是否被访问了,true:k
    public int movingCount(int m, int n, int k) {
        this.m = m; this.n = n; this.k = k;
        this.visited = new boolean[m][n];
        return dfs(0, 0);
    }
    public int dfs(int i, int j) {
        if(i >= m || j >= n || k < sum(i) + sum(j)|| visited[i][j]) return 0;
        visited[i][j] = true;
        return 1 + dfs(i + 1, j) + dfs(i, j + 1);
    }
	// 计算数位和
    public int sum(int x) {
        int sum = 0;
        while(x != 0) {
            s += x % 10;
            x = x / 10;
        }
        return sum;
    }
}
剑指 Offer 34. 二叉树中和为某一值的路径

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。

叶子节点 是指没有子节点的节点。

示例 1:

image-20220511143347613

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

// 二叉树 搜索问题
// 回溯法:先序遍历 + 路径记录
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
    recur(root, sum);
    return res;
}

void recur(TreeNode root, int tar) {
    if(root == null) return;
    path.add(root.val);
    tar -= root.val;
    if(tar == 0 && root.left == null && root.right == null)
        res.add(new LinkedList(path)); // 不能 res.add(path);
    recur(root.left, tar);
    recur(root.right, tar);
    // 添加的这个节点不行,回溯
    path.removeLast();// List 接口没有removeLast方法,LinkedList 有这个方法,或者用List的remove(path.size()-1)方法
}


剑指 Offer 54. 二叉搜索树的第k大节点

给定一棵二叉搜索树,请找出其中第 k 大的节点的值。

示例 1:

输入: root = [3,1,4,null,2], k = 1
3
/
1 4

2
输出: 4
示例 2:

输入: root = [5,3,6,2,4,null,null,1], k = 3
5
/
3 6
/
2 4
/
1
输出: 4

二叉搜索树:左 < 根 < 右

二叉搜索树的中序遍历为递增序列。根据此性质,易得二叉搜索树的 中序遍历倒序 为 递减序列 。
因此,求 “二叉搜索树第 kk 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。

// 中序遍历
// 打印中序遍历
void dfs(TreeNode root) {
 if(root == null) return;
 dfs(root.left); // 左
 System.out.println(root.val); // 根
 dfs(root.right); // 右
}
// 倒序即:按照 右 根 左 的顺序
class Solution {
    int res, k;
    public int kthLargest(TreeNode root, int k) {
        this.k = k;
        dfs(root);
        return res;
    }
    // 中序遍历 右 根 左
    void dfs(TreeNode root) {
        if(root == null) return;
        dfs(root.right);
        if(--k == 0){
            res = root.val;
            return;  
        }
        dfs(root.left);
    }
}

剑指 Offer 55 - I. 二叉树的深度

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

例如:

给定二叉树 [3,9,20,null,null,15,7],

​ 3

/
9 20
/
15 7
返回它的最大深度 3 。

树的遍历方式总体分为两类:深度优先搜索(DFS)、广度优先搜索(BFS);

常见 DFS : 先序遍历、中序遍历、后序遍历;
常见 BFS : 层序遍历(即按层遍历);

法1:DFS

public int maxDepth(TreeNode root) {
        if(root == null) return 0;
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}

法2:BFS

public int maxDepth(TreeNode root) {
    if(root == null) return 0;
    List<TreeNode> queue = new LinkedList<>() {{ add(root); }}, tmp;
    int res = 0;
    while(!queue.isEmpty()) {
        tmp = new LinkedList<>();
        for(TreeNode node : queue) {
            if(node.left != null) tmp.add(node.left);
            if(node.right != null) tmp.add(node.right);
        }
        queue = tmp;
        res++;
    }
    return res;
}

剑指 Offer 55 - II. 平衡二叉树

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

示例 1:

给定二叉树 [3,9,20,null,null,15,7]

​ 3

/
9 20
/
15 7
返回 true 。

示例 2:

给定二叉树 [1,2,2,3,3,null,null,4,4]

​ 1
​ /
​ 2 2
​ / \

3 3
/
4 4
返回 false 。

法1:先序遍历 (根 左右)+ 计算深度,再判断是否符合题目

class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) return true;
        return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }
	// 计算二叉树的深度
    private int depth(TreeNode root) {
        if (root == null) return 0;
        return Math.max(depth(root.left), depth(root.right)) + 1;
    }
}



剑指 Offer 64. 求1+2+…+n

求 1+2+…+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

示例 1:

输入: n = 3
输出: 6

//常规递归
public int sumNums(int n){
    if(n == 1) return 1;
    n += sumNums(n-1);
    return n;
}
//不用if 的递归,运用短路运算符
public static int sumNums(int n){
    boolean x = n > 1 &&  (n+=sumNums(n-1)) > Integer.MIN_VALUE;
    return n;
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
剑指 Offer 68 - II. 二叉树的最近公共祖先
剑指 Offer 37. 序列化二叉树
剑指 Offer 38. 字符串的排列

四、动态规划

包含了「分治思想」、「空间换时间」、「最优解」等多种基石算法思想

分治: 将原问题分解为 子问题,组合子问题的解 来得到原问题的解

动态规划:也通过组合子问题的解得到原问题的解。不同的是,适合用动态规划解决的问题具有「重叠子问题」和「最优子结构」两大特性。

重叠子问题 通过暴力算法 会有大量重复计算,效率低下。动态规划在第一次求解某子问题时,会将子问题的解保存,后续遇到重叠子问题时,则直接通过查表获取解,保证每个独立子问题只被计算一次

最优子结构:如果一个问题的最优解可以由其子问题的最优解组合构成,并且这些子问题可以独立求解,那么称此问题具有最优子结构。

动态规划从基础问题的解开始,不断迭代组合、选择子问题的最优解,最终得到原问题最优解。

剑指 Offer 10- I. 斐波那契数列

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

法1:暴力递归

int fibonacci(int n) {
    if (n == 0) return 0; // 返回 f(0)
    if (n == 1) return 1; // 返回 f(1)
    return fibonacci(n - 1) + fibonacci(n - 2); // 分解为两个子问题求解
}

法2:记忆化递归(剪枝)

// 重叠子问题 避免重复计算,辅助数组dp
int fibonacciDp(int n, int[] dp) {
    if (n == 0) return 0;           // 返回 f(0)
    if (n == 1) return 1;           // 返回 f(1)
    if (dp[n] != 0) return dp[n];   // 若 f(n) 以前已经计算过,则直接返回记录的解
    dp[n] = fibonacciDp(n - 1, dp) + fibonacciDp(n - 2, dp); // 将 f(n) 则记录至 dp
    return dp[n];
}


// 求第 n 个斐波那契数
int fibonacci(int n) {
    int[] dp = new int[n + 1]; // 用于保存 f(0) 至 f(n) 问题的解
    return fibonacciDp(n, dp);
}


法3:动态规划

// 递归本质上是基于分治思想的从顶至底的解法
//动态规划从底至顶
//转移方程 f(n) = f(n - 1) + f(n - 2)

int fibonacci(int n) {
    if (n == 0) return 0;       
    int[] dp = new int[n + 1];     // 初始化 dp 列表
    dp[1] = 1;                     // 初始化 f(0), f(1)
    for (int i = 2; i <= n; i++) { // 状态转移求取 f(2), f(3), ..., f(n) 
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];                  // 返回 f(n)
}

// 降低空间复杂度 O(N) --> O(1)
int fibonacci(int n) {
    if (n == 0) return 0;           // 若求 f(0) 则直接返回 0
    int a = 0, b = 1;               // 初始化 f(0), f(1)
    for (int i = 2; i <= n; i++) {  // 状态转移求取 f(2), f(3), ..., f(n) 
        int tmp = a;
        a = b;
        b = tmp + b;
    }
    return b;                       // 返回 f(n)
}

剑指 Offer 10- II. 青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

 // 状态转移方程:f(n) = f(n-1) + f(n-2)
 // f(0) = f(1) = 1; f(2) = 2;
 public int numWays(int n) {
        if(n==0 || n==1) return 1;
        int a = 1,b=1;
        int MOD =   1000000007;
        for(int i=2;i<=n;i++){
            int tmp = (a+b) % MOD;
            a = b;
            b = tmp;
        }
        return b;
    }
蛋糕最高售价

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q484nVyW-1654313778010)(…/…/AppData/Roaming/Typora/typora-user-images/image-20220406154059643.png)]

问题: 现给定一个重量为 nn 的蛋糕,问小力应该如何切分蛋糕,达到最高的蛋糕总售价。

法1:暴力递归

// f(n) = max{f(n),f(i) + p(n-1}
// 最高售价= Max( 最高售价,质量为 i 的最高售价 + 质量为 n - i 的普通售价
// 时间复杂度为指数级 O(2^n)
int f(int n,int[] priceList){
    if(n <= 1) return priceList[n]; // 蛋糕重量 <= 1 时直接返回
    int f_max = 0;
    for(int i = 0; i < n; i++){
        f_max = Math.max(f_max, f(i,priceList) + priceList[n-i]);
    }
    return f_max;
}

法2:记忆化递归

//时间复杂度: O(n^2)
// 多了一个辅助数组:dp[n+1]
int maxCakePrice2(int n,int[] priceList){
    int[] dp = new int[n + 1];
    return maxCakePriceDp(n,priceList,dp);
}

int maxCakePriceDp(int n, int[] priceList, int[] dp) {
    if(n <= 1) return priceList[n];
    int f_max = 0;
    for(int i = 0; i < n; i++){
        // f_i_max 表示质量为 i 的蛋糕的最高售价,dp数组有就不用重复计算,无则计算
        int f_i_max= dp[i] != 0 ? dp[i] : maxCakePriceDp(i,priceList,dp) ;
        f_max = Math.max(f_max,f_i_max + priceList[n-i]);
    }
    dp[n] = f_max;
    return f_max;
}

法3:动态规划

// 输入蛋糕价格列表 priceList ,求重量为 n 蛋糕的最高售价
int maxCakePrice(int n, int[] priceList) {
    if (n <= 1) return priceList[n];  // 蛋糕重量 <= 1 时直接返回
    int[] dp = new int[n + 1];     // 初始化 dp 列表,用来保存最高售价 dp(0) ~ dp(n)
    // 按顺序计算 dp[2] ~ dp[n]
    // dp[n] = Max{ dp[n],dp[i]+price[n-i]
    for (int j = 2; j <= n; j++) {    // 按顺序计算 f(1), f(2), ..., f(n)
        for (int i = 0; i < j; i++)   // 从 j 种组合种选择最高售价的组合作为 f(j)
            dp[j] = Math.max(dp[j], dp[i] + priceList[j - i]);
    }
    return dp[n];
}


普遍来看,求最值 的问题一般都具有「重叠子问题」和「最优子结构」特点,因此此类问题往往适合用动态规划解决。

动态规划解题框架:

1.状态定义 2.初始状态: 3.转移方程: 4.返回值

剑指 Offer 63. 股票的最大利润

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

法1:暴力

// 最大利益 = max(每天的最大利益)
// 每天的最大利益 = 当前价格 - 前n-1 天的最小价格
public int maxProfit(int[] prices) {
        int n = prices.length;
        int f_max = 0;
        for(int j=1;j<n;j++)
            for(int i = 0;i<j;i++)
                f_max = Math.max(f_max, prices[j] - prices[i]);
        return f_max;


    }

法3:动态规划,Max(选它,不选它)

// dp[i]=max(dp[i−1],prices[i]−min(prices[0:i]))
// 前i日最大利润=max(前(i−1)日最大利润,第i日价格−前i日最低价格)
public int maxProfit(int[] prices) {
		if(prices.length ==0) return 0;//数组为空,返回0
        int f_min = prices[0], max_prifit = 0;
        int n = prices.length;
        int[] dp = new int[n + 1];// 记录每天的最大利润

        for(int i=1; i < n; i++){
            // f_min 表示前 n -1个数的最小值
            f_min = Math.min(f_min, prices[i-1]);
            dp[i] = Math.max(dp[i-1], prices[i] - f_min);

        }
        return dp[n-1];// 返回第n天的最大利润

// 改:
/*
	由于 dp[i]只与 dp[i−1] , prices[i] , cost 相关,因此可使用一个变量(记为利润 profitprofit )代替 dpdp 列表。优化后的转移方程为:
	profit=max(profit,prices[i]−min(cost,prices[i])
*/
public int maxProfit(int[] prices) {
        int cost = Integer.MAX_VALUE, profit = 0;
        for(int price : prices) {
            cost = Math.min(cost, price);
            profit = Math.max(profit, price - cost);
        }
        return profit;
    }

剑指 Offer 42. 连续子数组的最大和

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

// 转移方程,选或者不选 q:以nums[i] 为结尾的连续数组
// 转移方程 dp[i] = Math.max(dp[i- 1] + nums[i], nums[i]);
public int maxSubArray(int[] nums) {
		int[] dp = new int[nums.length];
		dp[0] = nums[0];
		int max = nums[0];
		for (int i = 1; i < nums.length; i++) {
			dp[i] = Math.max(dp[i- 1] + nums[i], nums[i]);	
			max = Math.max(max,dp[i]);
		}
		return max;
	}
// 降低空间复杂度:
public int maxSubArray(int[] nums) {
    int res = nums[0];
    for(int i = 1; i < nums.length; i++) {
        nums[i] += Math.max(nums[i - 1], 0);
        res = Math.max(res, nums[i]);
    }
    return res;
}


剑指 Offer 47. 礼物的最大价值

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

// 注意是 m * n
//动态规划: dp[i][j] = 上一个礼物 最大值 + 现在的礼物值
// f(i,j)=max[f(i,j−1),f(i−1,j)]+grid(i,j)
 public int maxValue(int[][] grid) {
        int m = grid.length;
        int n= grid[0].length;
        int[][] dp = new int[m][n];

        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(i - 1 < 0 && j - 1 < 0) dp[i][j] = grid[i][j];
                else if(i - 1 < 0) dp[i][j] = dp[i][j-1] + grid[i][j];
                else if(j - 1 < 0) dp[i][j] = dp[i-1][j] + grid[i][j];
                else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + grid[i][j];
            }
        }

        return dp[m-1][n-1];
    }
// 降低空间复杂度,原地修改
 public int maxValue(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(i == 0 && j == 0) continue;
                if(i == 0) grid[i][j] += grid[i][j - 1] ;
                else if(j == 0) grid[i][j] += grid[i - 1][j];
                else grid[i][j] += Math.max(grid[i][j - 1], grid[i - 1][j]);
            }
        }
        return grid[m - 1][n - 1];
    }

 
剑指 Offer 46. 把数字翻译成字符串

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

法1:字符串

// 1.得到末尾2位数的值
// dp[i] = dp

class Solution {
   public static int translateNum(int num) {
        String s = ""+num;
        if(num < 10) return 1;
        int[] dp = new int[s.length()];
        dp[0] = 1;
        int tmp2 = (s.charAt(0)-'0')*10 + (s.charAt(1)-'0');// 计算以 num[i] 为末尾的末尾2位数
        dp[1]   = tmp2 > 25 ? 1 : 2;
        for(int i=2;i<s.length();i++){
            int tmp = (s.charAt(i-1)-'0')*10 + (s.charAt(i)-'0');// 计算以 num[i] 为末尾的末尾2位数
            if(tmp > 25 || tmp < 10) dp[i] = dp[i-1];
            else dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[s.length()-1];
    }
}
剑指 Offer 48. 最长不含重复字符的子字符串

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

输入: "abcabcbb"
输出: 3 
剑指 Offer 19. 正则表达式匹配

请实现一个函数用来匹配包含’. ‘和’‘的正则表达式。模式中的字符’.‘表示任意一个字符,而’'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但与"aa.a"和"ab*a"均不匹配。

示例 1:

输入:
s = “aa”
p = “a”
输出: false
解释: “a” 无法匹配 “aa” 整个字符串。

示例 2:

输入:
s = “aa”
p = “a*”
输出: true
解释: 因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。

示例 3:

输入:
s = “ab”
p = “."
输出: true
解释: ".
” 表示可匹配零个或多个(‘*’)任意字符(‘.’)。

示例 4:

输入:
s = “aab”
p = “cab”
输出: true
解释: 因为 ‘*’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。

示例 5:

输入:
s = “mississippi”
p = “misisp*.”
输出: false

注:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母以及字符 . 和 ,无连续的 ''。


剑指 Offer 49. 丑数

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

输入: n = 10
输出: 12
解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。
// 即Xn = min(前面一个各个数的2倍,3倍,5倍)
// 这个数从num[0]开始,每次 *2,*3,*5,下标就 + 1
 public int nthUglyNumber(int n) {
        int[] dp = new int[n]; // 存储丑数
        int a = 0, b = 0, c = 0;// 为*2,*3,*5的起始数字的下标
        dp[0] = 1;
        // 计算dp[1] ~ dp[n]
        for(int i = 1; i < n; i++){
            int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
            dp[i] = Math.min(Math.min(n2, n3) , n5); // 取前面3个数的2倍,3倍,5倍的最小值
            if(dp[i] == n2) a++;
            if(dp[i] == n3) b++;
            if(dp[i] == n5) c++;
        }
        return dp[n-1];
    }
剑指 Offer 60. n个骰子的点数

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

暴力O(6n)

动态规划

???

五、双指针(滑动窗口)

剑指 Offer 18. 删除链表的节点

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。

注意:此题对比原题有改动

示例 1:

输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.

 public ListNode deleteNode(ListNode head, int val) {
     if(head.val == val) return head.next;
     ListNode pre = head,cur = pre.next;//cur表示当前节点,pre表示前驱节点
     while(cur != null && cur.val != val){
         pre = pre.next;
         cur = pre.next;
     }
     // 当前节点非空,即找到了要删除的节点,删除该节点即可
     if(cur != null) pre.next = cur.next;
     return  head;
 }
剑指 Offer 22. 链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.

返回链表 4->5.

暴力求出链表的长度,再遍历是第几个节点

 public ListNode getKthFromEnd(ListNode head, int k) {
        int length = 0;
        ListNode p = head;
        while(p != null){
            p = p.next; length++;
        }
        length = length - k + 1;
        //System.out.println(length);
        p = head;
        while(--length != 0){
            p =p.next;
        }
        return p;
    }

法2:双指针,双指针 formerlatter 间相距 k

 public ListNode getKthFromEnd(ListNode head, int k) {
       ListNode former = head,cur = head;
       for(int i= 0; i < k; i++) cur = cur.next;
       while(cur != null){
           cur = cur.next;
           former = former.next;
       }
       return former;
    }
剑指 Offer 25. 合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。

示例1:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
        ListNode head = new ListNode(0),cur = head;// head表示头节点,cur表示当前节点
        while(l1 != null && l2 != null){
            // l1 节点对应的值小,cur.next 就l1,否则就取 l2
            if(l1.val < l2.val){
                cur.next = l1;
                l1 = l1.next;
            }else{
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }
        // 如何l1 l2 有一方为空,则合并 剩余节点
        cur.next =  l1 != null ? l1 : l2;
        return head.next;
    }
剑指 Offer 52. 两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点。

如下面的两个链表**:**

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fAXs1EQT-1654313778011)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20220515155155426.png)]

在节点 c1 开始相交。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode A = headA, B = headB;
    while (A != B) {
        A = A != null ? A.next : headB;
        B = B != null ? B.next : headA;
    }
    return A;
}

 

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。

示例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4]
注:[3,1,2,4] 也是正确的答案之一。

public static int[] exchange(int[] nums) {
        int first=0,last = nums.length - 1, tmp;
        while(first < last){
            // first >= last 提前退出,first 找偶数,last找奇数
           while(first < last && (nums[first] & 1) == 1 ) first++;
           while(first < last && (nums[last] & 1) == 0 ) last--;
           // 交换奇数 和 偶数
           tmp = nums[first];
           nums[first] = nums[last];
           nums[last] = tmp;
        }
        return nums;
    }
剑指 Offer 57. 和为s的两个数字

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[2,7] 或者 [7,2]

双指针碰撞

public int[] twoSum(int[] nums, int target) {
    int i = 0, j = nums.length - 1;
    while(i < j) {
        int s = nums[i] + nums[j];
        if(s < target) i++;
        else if(s > target) j--;
        else return new int[] { nums[i], nums[j] };
    }
    return new int[0];
}

剑指 Offer 58 - I. 翻转单词顺序

输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。

示例 1:

输入: “the sky is blue”
输出: “blue is sky the”

法1:双指针,倒序遍历字符串,添加单词

public String reverseWords2(String s) {
        s = s.trim();                                    // 删除首尾空格
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while (i >= 0) {
            while (i >= 0 && s.charAt(i) != ' ') i--;     // 搜索首个空格
            res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
            while (i >= 0 && s.charAt(i) == ' ') i--;     // 跳过单词间空格
            j = i;                                       // j 指向下个单词的尾字符
        }
        return res.toString().trim();                    // 转化为字符串并返回
    }

法2:分隔 + 倒序 (内置函数,面试不建议使用)

public String reverseWords(String s) {
    String[] strs = s.trim().split(" ");        // 删除首尾空格,分割字符串
    StringBuilder res = new StringBuilder();
    for (int i = strs.length - 1; i >= 0; i--) { // 倒序遍历单词列表
        if(strs[i].equals("")) continue;        // 遇到空单词则跳过
        res.append(strs[i] + " ");              // 将单词拼接至 StringBuilder
    }
    return res.toString().trim();               // 转化为字符串,删除尾部空格,并返回
}

六、排序

剑指 Offer 45. 把数组排成最小的数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

剑指 Offer 61. 扑克牌中的顺子

从若干副扑克牌中随机抽 5 张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

示例 1:

输入: [1,2,3,4,5]
输出: True

剑指 Offer 40. 最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

剑指 Offer 41. 数据流中的中位数

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

例如,

[2,3,4] 的中位数是 3

[2,3] 的中位数是 (2 + 3) / 2 = 2.5

设计一个支持以下两种操作的数据结构:

void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数

示例 1:

输入:
[“MedianFinder”,“addNum”,“addNum”,“findMedian”,“addNum”,“findMedian”]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]

七、分治算法

剑指 Offer 07. 重建二叉树

输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。

假设输入的前序遍历和中序遍历的结果中都不含重复的数字。

public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}

示例 1:

Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]

public TreeNode buildTree(int[] preorder, int[] inorder) {

    }
剑指 Offer 16. 数值的整数次方

实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

暴力

超大数字,2^31-1; int的范围是 -231——231-1,如果a = -231 ,不能直接*-1 会越界

解决方法:n存入 long b中

超时:解决办法:用快速幂O(log n)

法2:递归

// jvm规定了栈的最大深度,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
public static double myPow(double x, int n){
    if(n == 0) return 1;
    if(n < 0) return 1 / myPow(x, -n);
    if(n %2 == 1) return  x * myPow(x, n-1);// n是奇数
    double tmp = myPow(x,n/2);// n为偶数 二分
    return tmp * tmp;
}

法3:快速幂

public double myPow(double x, int n) {
    if(x == 0.0f) return 0.0d;
    long b = n;//当n是负数时 执行取反可能会越界 所以需要用一个long整形的变量来存储n
    double res = 1.0;
    if(b < 0){
        x = 1 / x;
        b = -b;
    }
    while(b > 0){
        if((b & 1) == 1) res *= x;//当b是奇数时 等价于b % 2 ==1
        x *= x;
        b >>= 1;//等价于b /= 2;
    }
    return res;
}
//注: 当 x = 0.0 时:直接返回 0.0 ,以避免后续 1 除以 0 操作报错。
剑指 Offer 17. 打印从1到最大的n位数

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

示例 1:

输入: n = 1
输出: [1,2,3,4,5,6,7,8,9]

法1:暴力

 public int[] printNumbers(int n) {
        int num =(int) Math.pow(10,n) -1;
         int[] res = new int[num-1];
         for (int i = 0; i < num; i++) {
             res[i] = i+1;
         }
        return res;
    }
// 但由于本题要求返回 int 类型数组,相当于默认所有数字都在 int32 整型取值范围内,因此不考虑大数越界问题。

大数打印解法:字符串 全排列

class Solution {
    StringBuilder res;
    int count = 0, n;
    char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
    public String printNumbers(int n) {
        this.n = n;
        res = new StringBuilder(); // 数字字符串集
        num = new char[n]; // 定义长度为 n 的字符列表
        dfs(0); // 开启全排列递归
        res.deleteCharAt(res.length() - 1); // 删除最后多余的逗号
        return res.toString(); // 转化为字符串并返回
    }
    
    //全排列
    void dfs(int x) {
        if(x == n) { // 终止条件:已固定完所有位
            res.append(String.valueOf(num) + ","); // 字符数组转字符串,拼接 num 并添加至 res 尾部,使用逗号隔开
            return;
        }
        for(char i : loop) { // 遍历 ‘0‘ - ’9‘
            num[x] = i; // 固定第 x 位为 i
            dfs(x + 1); // 开启固定第 x + 1 位
        }
    }
}


剑指 Offer 33. 二叉搜索树的后序遍历序列

输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。

tips:因为后序序列无法唯一确定一棵二叉树。只要存在一个二叉搜索树 match 这个后续遍历结果,就返回 true

补:

二叉搜索树和二叉排序树是一样的,英文全称是“Binary Search Tree”。 左 < 根 < 右

后序遍历 最后一个元素 是根元素

参考以下这颗二叉搜索树:

​ 5
​ / \

2 6
/
1 3
示例 1:

输入: [1,6,3,2,5]
输出: false
示例 2:

输入: [1,3,2,6,5]
输出: true

public boolean verifyPostorder(int[] postorder) {
    return recur(postorder, 0, postorder.length - 1);
}
boolean recur(int[] postorder, int i, int j) {
    if(i >= j) return true;
    int p = i;
    while(postorder[p] < postorder[j]) p++;// 寻找第一个大于根节点的数的索引,即m
    int m = p; 
    while(postorder[p] > postorder[j]) p++; // 判断右子树是否全部大于根节点,即p==j,不满足返回false,满足判断左右子树
    // i、j为左右两边的边界 j为根节点索引 ,m为第一个大于根节点数的索引,左子树 [j,m-1]  右子树 [m,j-1]
    return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
}
剑指 Offer 51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

法1:暴力

// 超时
public int reversePairs(int[] nums) {
    int count = 0;
    for(int i= 0;i<nums.length-1;i++)
        for(int j= i+1;j<nums.length;j++)
            if(nums[i] > nums[j])
                count++;
    return count;
}

法2:归并排序 分而治之

// 归并排序 模板
private static void mergeSort(int[] q, int l, int r) {
    if (l >= r) return;
    // 确定分界点
    int mid =  (l + r) >> 1;

    // 递归左右
    mergeSort(q, l, mid);
    mergeSort(q, mid + 1, r);

    // 归并 -- 合二为一
    int i = l, j = mid + 1, k = 0;
    int[] tmp = new int[r - l + 1];
    while (i <= mid && j <= r) {
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else tmp[k++] = q[j++];
    }
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];

    // 复制回原数组
    for (int m = l, n = 0; m <= r; m++, n++) { // 这里需要注意
        q[m] = tmp[n];
    }
}
// 修改原 mergeSort 代码,使得 mergeSort(q, l, r) 得到的是 q[l ~ r] 之间的逆序对
// 逆序对本身由三部分组成,left 中的逆序对 + right 中的逆序对 + 横跨 left、right 的逆序对
// 横跨left、right 的逆序对与left right 本身的顺序无关,所以对 left、right 进行排序,减小复杂度
public int reversePairs(int[] nums) {
    return mergeSort(nums, 0, nums.length - 1);
}
private static int mergeSort(int[] q, int l, int r) {
    if (l >= r) return 0; // 这里改成 return 0
    // 找分界点
    int mid = (l + r) >> 1;

    // 递归 mergeSort 左右
    int left = mergeSort(q, l, mid);  // left 中的逆序对数目
    int right = mergeSort(q, mid + 1, r); // right 中的逆序对数目

    int[] tmp = new int[r - l + 1];
    int i = l, j = mid + 1, k = 0;
    // 计算横跨 left、right 的逆序对
    int both = 0;

    while (i <= mid && j <= r) {
        if (q[i] <= q[j]) tmp[k++] = q[i++];
        else {
            tmp[k++] = q[j++];
            both += (mid - i + 1);   // 这里多加一行
            // 比如 456|123,right 的 1 进 tmp,那么就产生了 3 个逆序对,2 进 tmp,产生三个逆序对,。。。
            // 规律就是当前 left 中剩余元素的数目,可以用 mid - i + 1 来计算
        }
    }
    while (i <= mid) tmp[k++] = q[i++];
    while (j <= r) tmp[k++] = q[j++];

    // 复制回 q
    for (int a = l, b = 0; a <= r; a++, b++) {
        q[a] = tmp[b];
    }
    return left + right + both;
}

八、位运算

剑指 Offer 15. 二进制中1的个数

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。

示例 1:

输入:n = 11 (控制台输入 00000000000000000000000000001011)
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

法1:循环判断

public static int hammingWeight2(int n){
        int count = 0;
        while(n != 0){
            count += n&1;// 加上n二进制的最后一位
            n >>>= 1;//向右移一位,注:Java 中无符号右移为 ">>>"
        }
        return count;
    }

“>>>”运算符所作的是无符号的位移处理,它不会将所处理的值的最高位视为正负符号,所以作位移处理时,会直接在空出的高位填入0。

剑指 Offer 56 - I. 数组中数字出现的次数

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]

法1:排序 + 暴力

 public int[] singleNumbers(int[] nums) {
        Arrays.sort(nums);
        int[] res = new int[2];
        int i=1,j = 0;
        while(i<nums.length){
            if(nums[i] != nums[i-1]) {
                res[j++] = nums[i-1];
                i++;
            }else i+=2;
        }
        if(i == nums.length ) res[j] = nums[i-1];
        return res;
    }

法2:位运算

异或满足交换律,相同元素异或为0, 0^x = x

a ⊕ a ⊕ b ⊕ b ⊕ x = x

3^4 =7—> 011 ^ 100 = 111

public int[] singleNumbers(int[] nums) {
    int x = 0, y = 0, n = 0, m = 1;
    for(int num : nums)               // 1. 遍历异或
        n ^= num;
    while((n & m) == 0)               // 2. 循环左移,计算 m
        m <<= 1;
    for(int num: nums) {              // 3. 遍历 nums 分组
        if((num & m) != 0) x ^= num;  // 4. 当 num & m != 0
        else y ^= num;                // 4. 当 num & m == 0
    }
    return new int[] {x, y};          // 5. 返回出现一次的数字
}
剑指 Offer 56 - II. 数组中数字出现的次数 II

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

示例 1:

输入:nums = [3,4,3,3]
输出:4

public int singleNumber(int[] nums) {
    int[] counts = new int[32];
    for(int num : nums) {
        for(int i = 0; i < 32; i++) {
            counts[i] += num & 1; // 更新第 i 位 1 的个数之和
            num >>= 1;            // 第 i 位 --> 第 i 位
        }
    }
    int res = 0, m = 3;
    for(int i = 31; i >= 0; i--) {
        res <<= 1;
        res |= counts[i] % m;     // 恢复第 i 位
    }
    return res;
}
剑指 Offer 65. 不用加减乘除做加法

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

提示:

  • a, b 均可能是负数或 0
  • 结果不会溢出 32 位整数

示例:

输入: a = 1, b = 1
输出: 2

public int add(int a, int b) {
    while(b != 0) { // 当进位为 0 时跳出
        int c = (a & b) << 1;  // c = 进位
        a ^= b; // a = 非进位和
        b = c; // b = 进位
    }
    return a;
}

九、数学

剑指 Offer 39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

投票法

public int majorityElement(int[] nums) {
    int x = 0, votes = 0;
    for(int num : nums){
        if(votes == 0) x = num;
        votes += num == x ? 1 : -1;
    }
    return x;
}
剑指 Offer 66. 构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

示例:

输入: [1,2,3,4,5]
输出: [120,60,40,30,24]

剑指 Offer 14- I. 剪绳子

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

剑指 Offer 57 - II. 和为s的连续正数序列

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]

剑指 Offer 62. 圆圈中最后剩下的数字

0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

示例 1:

输入: n = 5, m = 3
输出: 3

剑指 Offer 14- II. 剪绳子 II

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

剑指 Offer 43. 1~n 整数中 1 出现的次数

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

示例 1:

输入:n = 12
输出:5

剑指 Offer 44. 数字序列中某一位的数字

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

示例 1:

输入:n = 3
输出:3

十、模拟

剑指 Offer 29. 顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。

示例 1:

输入:matrix = [[1,2,3],

​ [4,5,6],

​ [7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

// 顺时针打印:从左向右、从上向下、从右向左、从下向上  循环。
public static 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;
    int[] res = new int[(right + 1) * (bottom + 1)];
    int count = 0;
    while(true){
        for(int i = left ;i <= right; i++) res[count++] = matrix[top][i];// 从左到右
        if(++top > bottom) break; 从左到右之后,上边界加1,判断是否越界
        for(int i = top;i<=bottom;i++) res[count++] = matrix[i][right];//从上到下
        if(--right < left) break;
        for(int i=right; i>= left;i--) res[count++] = matrix[bottom][i];//从右到左
        if(--bottom < top) break;
        for(int i=bottom;i>=top;i--) res[count++] = matrix[i][left];//从下到上
        if(++left > right) break;
    }
    return res;
}
剑指 Offer 31. 栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {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

public boolean validateStackSequences(int[] pushed, int[] popped) {
    Stack<Integer> stack = new Stack<>();
    int i = 0;
    for(int num : pushed) {
        stack.push(num); // num 入栈
        while(!stack.isEmpty() && stack.peek() == popped[i]) { // 循环判断与出栈: 栈顶元素等于 popped当前元素就出栈
            stack.pop();
            i++;
        }
    }
    return stack.isEmpty();
}

考虑的问题

数组为null,活着数组长度为0,n=0的情况

数组中数字不符合题意

if(root == null) return new int[0];

注:数组不都是 n*n的,有的题目要求是 m * n

方法:

双栈法、辅助栈、递归
1. 排序法
2. 数组打卡标记
3. 原地交换
4. 哈希

常用函数

for(Character c : String.toCharArray())
StringBuilder:toString(),append();
Arrays.sort(nums);
LinkedList.removeLast(),addFirst() ,addLast()
Stack:isEmpty(),pop(),push() ,search()  

Queue

add()和remove()方法在失败的时候会抛出异常(不推荐)
offer()、:添加元素并返回true
	一些队列有大小限制,满队列 加新的项目,offer()不抛出一个 unchecked 异常,得到由 offer() 返回的 false。
poll() 删除元素并返回头部元素,用空集合调用时不是抛出异常,只是返回 null
peek() 查看头部元素,在队列为空时,返回null
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值