leetcode刷题笔记——剑指offer(二)[回溯、排序、位运算、数学、字符串]

搜索与回溯

剑指 Offer 12. 矩阵中的路径

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

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

例如,在下面的 3×4 的矩阵中包含单词 “ABCCED”(单词中的字母已标出)。

在这里插入图片描述

示例 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
 

提示:

1 <= board.length <= 200
1 <= board[i].length <= 200
board 和 word 仅由大小写英文字母组成

我的思路:
版本1:未加备忘录进行剪枝,会导致代码超时。
【{注意事项}】 当count作为传入参数时,不要返回参数count,逻辑容易混乱。
超时原因:
1、fill函数的参数image是一个二维矩阵,每次递归都会复制一个相同大小的矩阵进行操作,非常耗时
2、dfs不进行剪枝,会走很多重复路径。

修改 => 版本二,如下:
1、设置一个vector<vector<int>> mem备忘录用于记录当前位置不能通过的count数
错误原因:
{‘A’, ‘B’, ‘C’, ‘E’},
{‘S’, ‘F’, ‘E’, ‘S’},
{‘A’, ‘D’, ‘E’, ‘E’}
“ABCESEEEFS”
‘A’ > ‘B’ > ‘C’ v ‘E’ > ‘S’的时候,mem会记录5为不能通过的count数
但是正确路线’A’ > ‘B’ > ‘C’ > ‘E’ > 'S’需要经过count =5 ❌

	bool inArea(vector<vector<char>> &image, int x, int y)
    {
        return x >= 0 && x < image.size() && y >= 0 && y < image[0].size();
    }
    
	bool fill(vector<vector<char>> image, int x, int y, string target, int count, vector<vector<set<int>>>& mem) //👀1
    {
        if(count == target.size()) return true;
        if (!inArea(image, x, y)) return false;
        if (image[x][y] != target[count] || mem[x][y].count(count)) return false;
        image[x][y] = ' ';
        count++;
        if (fill(image, x - 1, y, target, count, mem)||
        fill(image, x + 1, y, target, count, mem)||
        fill(image, x, y - 1, target, count, mem)||
        fill(image, x, y + 1, target, count, mem)) return true;
        else mem[x][y].insert(count); 
        return false;
    }

    bool exist(vector<vector<char>>& board, string word) {
        vector<vector<set<int>>> mem(board.size(), vector<set<int>>(board.front().size()));
        int count = 0;
        if(board.size()*board.front().size() < word.size()) return false;
        vector<int> x, y;
        for (int i = 0; i < board.size(); ++i)
        {
            for (int j = 0; j < board[0].size(); ++j)
            {
                if (board[i][j] == word[0])
                {
                    x.push_back(i);
                    y.push_back(j);
                }
            }
        }

        for (int i = 0; i < x.size(); ++i)
        {
            vector<vector<char>> tmp = board;
            if(fill(tmp, x[i], y[i], word, count, mem)) return true;
        }
        return false;
    }

修改版本三
使用 vector<vector<bool>> visited(board.size(), vector<bool>(board.front().size())); 代替memo,
此时我们可以不改变image,而通过visited来判断当前路径是否走过。
fill传入的image改为引用&image

	bool inArea(vector<vector<char>> &image, int x, int y)
    {
        return x >= 0 && x < image.size() && y >= 0 && y < image[0].size();
    }
    bool fill(vector<vector<char>> &image, int x, int y, string &target, int count, vector<vector<bool>> &visited)
    {
        if (count == target.size()) return true;
        if (!inArea(image, x, y)) return false;
        if (image[x][y] != target[count] || visited[x][y]) return false;
        visited[x][y] = true;
        if (fill(image, x - 1, y, target, count+1, visited) ||
            fill(image, x + 1, y, target, count+1, visited) ||
            fill(image, x, y - 1, target, count+1, visited) ||
            fill(image, x, y + 1, target, count+1, visited)) return true;
        visited[x][y] = false; // 若未找到,则将该位置改为false
        return false;
    }

    bool exist(vector<vector<char>>& board, string word) {
        vector<vector<bool>> visited(board.size(), vector<bool>(board.front().size()));
        int count = 0;
        if(board.size()*board.front().size() < word.size()) return false;
        vector<int> x, y;
        for (int i = 0; i < board.size(); ++i)
        {
            for (int j = 0; j < board[0].size(); ++j)
            {
                if (board[i][j] == word[0])
                {
                    x.emplace_back(i);
                    y.emplace_back(j);
                }
            }
        }
        for (int i = 0; i < x.size(); ++i)
        {
            if(fill(board, x[i], y[i], word, count, visited)) return true;
        }
        return false;
    }

剑指 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
示例 2:

输入:m = 3, n = 1, k = 0
输出:1
提示:

1 <= n,m <= 100
0 <= k <= 20

我的方法:dfs+备忘录剪枝

class Solution {
public:
    int count=0; 
    int movingCount(int m, int n, int k) {
        vector<vector<bool>> visited(m, vector<bool>(n));
        dfs(visited, 0, 0, k);
        return this->count;
    }
    void dfs(vector<vector<bool>> &visited, int x, int y, int k)
    {
        if (!inArea(visited, x, y)) return;
        if (visited[x][y]==true) return;
        if (get(x) + get(y) > k) return;
        ++this->count;
        visited[x][y] = true;
        dfs(visited, x-1, y, k);
        dfs(visited, x+1, y, k);
        dfs(visited, x, y-1, k);
        dfs(visited, x, y+1, k);
        // visited[x][y] = false; // 此处不能将visited还原,因为我们不需要重新计数该点
        return;
    }
    bool inArea(vector<vector<bool>> &visited, int x, int y)
    { return x>=0 && x<visited.size() && y>=0 && y<visited.front().size(); }
    //得到x的位数和
    int get(int x) { 
	    int res=0;
	    for (; x; x /= 10) {
	        res += x % 10;
	    }
	    return res;
    }
};

复杂度分析:
设矩阵行列数分别为 M, N 。

时间复杂度 O(MN): 最差情况下,机器人遍历矩阵所有单元格,此时时间复杂度为 O(MN) 。
空间复杂度 O(MN): 最差情况下,Set visited 内存储矩阵所有单元格的索引,使用 O(MN) 的额外空间。

剑指 Offer 34. 二叉树中和为某一值的路径

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

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

示例 1:



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



输入:root = [1,2,3], targetSum = 5
输出:[]
示例 3:

输入:root = [1,2], targetSum = 0
输出:[]
 

提示:

树中节点总数在范围 [0, 5000] 内
-1000 <= Node.val <= 1000
-1000 <= targetSum <= 1000

我的方法:dfs
注意//👀处的区别,减轻时间与空间复杂度的关键
不加引用,每次都将复制sample,耗时耗地
加了引用,每次返回 手动pop掉多出来的节点值,极大节约时间空间

    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<vector<int>> ans;
        vector<int> sample;
        dfs(root, target, ans, sample);
        return ans;
    }
    void dfs(TreeNode* root, int target, vector<vector<int>>& ans, vector<int>& sample)//👀
    {
        if (!root) return;
        if (target-root->val==0 && !root->left && !root->right) 
        {
            sample.emplace_back(root->val);
            ans.emplace_back(sample);
            sample.pop_back();//👀
            return;
        }
        sample.emplace_back(root->val);
        dfs(root->left, target-root->val, ans, sample);
        dfs(root->right, target - root->val, ans, sample);
        sample.pop_back();//👀
        return;
    }
    // void dfs(TreeNode* root, int target, vector<vector<int>>& ans, vector<int> sample)//👀
    // {
    //     if (!root) return;
    //     if (target-root->val==0 && !root->left && !root->right) 
    //     {
    //         sample.emplace_back(root->val);
    //         ans.emplace_back(sample);
    //         return;
    //     }
    //     sample.emplace_back(root->val);
    //     dfs(root->left, target-root->val, ans, sample);
    //     dfs(root->right, target - root->val, ans, sample);
    //     return;
    }

复杂度分析

时间复杂度: O ( N 2 ) O(N^2) O(N2),其中 N 是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2)。

空间复杂度:O(N),其中 N 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。

剑指 Offer 36. 二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

为了让您更好地理解问题,以下面的二叉搜索树为例:
在这里插入图片描述

我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
在这里插入图片描述

特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。

我的思路:用一个vector存取中序遍历的节点值

	Node* treeToDoublyList(Node* root) {
        if (!root) return root;
        vector<Node*> v;
        dfs(root, v);
        for (int i=0; i<v.size(); ++i)
        {
            if (i<v.size()-1){
                v[i+1]->left = v[i];
                v[i]->right = v[i+1];
            }else {
                v[0]->left = v[i];
                v[i]->right = v[0];
            }
        }
        return v[0];
    }
    void dfs(Node* root, vector<Node*>& v)
    {
        if (!root) return;
        dfs(root->left, v);
        v.emplace_back(root);
        dfs(root->right, v);
    }

时间复杂度:O(n)
空间复杂度:O(n)

方法二:inorder + 直接修改节点指向
本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
将 二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:

  1. 排序链表: 节点应从小到大排序,因此应使用 中序遍历 “从小到大”访问树的节点。
  2. 双向链表: 在构建相邻节点的引用关系时,设前驱节点 pre 和当前节点 cur ,不仅应构建 pre->right = cur ,也应构建 cur->left = pre
  3. 循环链表: 设链表头节点 head 和尾节点 tail ,则应构建 head->left = tailtail->right = head
class Solution {
public:
    Node* treeToDoublyList(Node* root) {
        if(root == nullptr) return nullptr;
        dfs(root);
        // dfs结束后,pre指向二叉搜索树的最大节点,即最右端
        head->left = pre;
        pre->right = head;
        return head;
    }
private:
    Node *pre, *head;//👀 维护一个全局的pre
    void dfs(Node* cur) {
        if(cur == nullptr) return;
        dfs(cur->left);
        if(pre != nullptr) pre->right = cur; //👀
        else head = cur;//👀
        cur->left = pre;//👀
        pre = cur;//👀
        dfs(cur->right);
    }
};

时间复杂度:O(n)
空间复杂度:O(n) 最糟糕情况,二叉树排成链状,stack需要存n个结点

剑指 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
 

限制:

1 ≤ k ≤ 二叉搜索树元素个数

我的思路:用一个vector 从大到小存元素,存到第k个后跳出
从右向左遍历二叉顺序树

    int kthLargest(TreeNode* root, int k) {
        vector<int> res;
        inorder(root, res, k);
        return res.back();
    }
    void inorder(TreeNode* root, vector<int> &res, int &k)
    {
        if (!root) return;
        inorder(root->right, res, k);
        if (res.size()<k) res.emplace_back(root->val);
        else return;
        inorder(root->left, res, k);
    }

复杂度分析:
时间复杂度 O(N) : 当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 N ,占用 O(N) 时间。
空间复杂度 O(N) : 当树退化为链表时(全部为右子节点),系统使用 O(N) 大小的栈空间。还有一个vector的额外空间

思路二:直接设定类属性res,当遍历到第k大的结点时,res=root->val即可

	int res;
	int kthLargest(TreeNode* root, int k) {
        inorder(root, k);
        return res;
    }
    void inorder(TreeNode* root, int &k)
    {
        if (!root) return;
        inorder(root->right, k);
        --k;
        if (k==0) res = root->val;
        else if (k<0) return;
        inorder(root->left, k);
    }   

复杂度分析:
时间复杂度 O(N) : 当树退化为链表时(全部为右子节点),无论 k 的值大小,递归深度都为 N ,占用 O(N) 时间。
空间复杂度 O(N) : 当树退化为链表时(全部为右子节点),系统使用 O(N) 大小的栈空间。

排序

(快排)912. Sort an Array

Given an array of integers nums, sort the array in ascending order.

Example 1:

Input: nums = [5,2,3,1]
Output: [1,2,3,5]
Example 2:

Input: nums = [5,1,1,2,0,0]
Output: [0,0,1,1,2,5]
 

Constraints:

1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

快速排序
每次的base都选择left
问题:当数组为有序数组时,效率会非常低下
改进:随机找一个数作为base,并将其换到left的位置
rand()
1、rand()不需要参数,它会返回一个从0到最大随机数的任意整数,最大随机数的大小通常是固定的一个大整数。
2、如果你要产生0~99这100个整数中的一个随机整数,可以表达为:int num = rand() % 100;

	def sortArray(nums: List[int]):
        def swap(i: int, j: int):
            nums1[i], nums1[j] = nums1[j], nums1[i]
        def partition(nums: List[int], l: int, r: int)->int:
            pivot = l			# 👀 标定物,只动一次,最后动
            index = l+1			# 👀 index左边全是小于 pivot 的值
            l += 1				# 👀 用于遍历数组
            while l <= r:
                if nums[l]<nums[pivot]:
                    swap(l, index)
                    index += 1
                l += 1
            swap(index-1, pivot)# index 最后一次比较多加了 1,要减掉
            return index-1		# 👀 index-1 是标定物的坐标
        def quickSort(nums: List[int], l: int, r: int):
            if l>=r:
                return
            pivot = partition(nums, l, r)
            quickSort(nums, l, pivot-1)	# 👀, pivot 可以不参与比较
            quickSort(nums, pivot+1, r) # 👀, pivot 可以不参与比较
            
       	quickSort(nums, 0, len(nums))

复杂度分析

时间复杂度:基于随机选取主元的快速排序时间复杂度为期望 O ( n log ⁡ n ) O(n\log n) O(nlogn),其中 n 为数组的长度。详细证明过程可以见《算法导论》第七章,这里不再大篇幅赘述。

空间复杂度: O ( h ) O(h) O(h),其中 h h h 为快速排序递归调用的层数。我们需要额外的 O ( h ) O(h) O(h) 的递归调用的栈空间,由于划分的结果不同导致了快速排序递归调用的层数也会不同,最坏情况下需 O ( n ) O(n) O(n) 的空间,最优情况下每次都平衡,此时整个递归树高度为 log ⁡ n \log n logn,空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

在这里插入图片描述

(归并) 148. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。

示例 1:


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


输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]
示例 3:

输入:head = []
输出:[]
 

提示:

链表中节点的数目在范围 [0, 5 * 104] 内
-105 <= Node.val <= 105
 

进阶:你可以在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?

思路:归并

  1. 找中间节点,注意细节,需要返回中间靠左的节点
  2. dfs
  3. merge
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        def find_mid(node: ListNode)->ListNode:
            slow, fast = node, node.next 	# 👀 fast 注意是next,可以让slow最终偏左
            while fast and fast.next:
                slow = slow.next
                fast = fast.next.next
            return slow

        def merge(node1: ListNode, node2: ListNode)->ListNode:
            dummy = ListNode(-1)
            cur = dummy
            while node1 and node2:
                if node1.val<=node2.val:
                    cur.next = node1
                    node1 = node1.next
                else:
                    cur.next = node2
                    node2 = node2.next
                cur = cur.next
            if node1:
                cur.next = node1
            if node2:
                cur.next = node2
            return dummy.next

        def dfs(node: Optional[ListNode])->Optional[ListNode]:
            if not node or not node.next:
                return node
            mid = find_mid(node)
            node2 = mid.next
            mid.next = None
            left = dfs(node)
            right = dfs(node2)
            return merge(left, right)
        return dfs(head)

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

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: "102"
示例 2:

输入: [3,30,34,5,9]
输出: "3033459"
 

提示:

0 < nums.length <= 100
说明:

输出结果可能非常大,所以你需要返回一个字符串而不是整数
拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

我的方法:快排,py
新加的地方:👀,其他都是快排模板

class Solution:
    def minNumber(self, nums: List[int]) -> str:
        nums = list(map(str, nums))
        def compare(s1: str, s2: str)->bool: # 👀
            return int(s1+s2)<=int(s2+s1)
            
        def swap(nums: list, i, j):
            nums[i], nums[j] = nums[j], nums[i]
        def partition(nums: list, l: int, r: int)->int:
            pivot = l
            idx = l+1
            l = idx
            while l<=r:
                if compare(nums[l], nums[pivot]):
                    swap(nums, idx, l)
                    idx += 1
                l += 1
            swap(nums, pivot, idx-1)
            return idx-1
        def quickSort(nums: list, l: int, r: int):
            if l>=r: return
            pivot = partition(nums, l, r)
            quickSort(nums, l, pivot-1)
            quickSort(nums, pivot+1, r)

        quickSort(nums, 0, len(nums)-1)
        return "".join(nums)

快速排序
此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y ,则规定 排序判断规则 为:

若拼接字符串 x + y > y + x ,则 x “大于” y ;
反之,若 x + y < y + x ,则 x “小于” y ;
x “小于” y 代表:排序完成后,数组中 x 应在 y 左边;“大于” 则反之。

由于本题nums长度不大,故未使用上题的优化方法

public:
    string minNumber(vector<int>& nums) {
        vector<string> res;
        for (int i=0; i<nums.size(); ++i)
        {
            res.emplace_back(to_string(nums[i]));
        }
        quickSort(res, 0, res.size()-1);
        string s;
        for (int i=0; i<res.size(); ++i)
        {
            s+=res[i];
        }
        return s;
    }
private:
    void quickSort(vector<string> & res, int left, int right){
        if (left>=right) return;
        int i = left, j = right;
        string base = res[left];;
        while (i<j)
        {
            while (res[j] + base >=  base + res[j] && i<j) --j;
            while (res[i] + base <=  base + res[i] && i<j) ++i;
            swap(res[i], res[j]);
        }
        res[left] = res[i];
        res[i] = base;
        quickSort(res, left, i-1);
        quickSort(res, i+1, right);
        return;
    }

剑指 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]
示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]
 

限制:
 
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000

我的思路:快排后赋值vector

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        quickSort(arr, 0, arr.size()-1);
        return vector<int>(arr.begin(), arr.begin()+k);
    }
    void quickSort(vector<int> &arr, int left, int right)
    {
        if (left>=right) return;
        int i=left, j=right;
        swap(arr[left], arr[rand()%(right-left+1)+left]);
        int base = arr[left];
        while (i<j)
        {
            while(i<j && arr[j]>=base) j--;
            while(i<j && arr[i]<=base) i++;
            swap(arr[i], arr[j]);
        }
        arr[left] = arr[i];
        arr[i] = base;
        quickSort(arr, left, i-1);
        quickSort(arr, i+1, right);
        return;
    }
};

复杂度分析

时间复杂度:O(n\log n),其中 n 是数组 arr 的长度。算法的时间复杂度即排序的时间复杂度。

空间复杂度:O(\log n),排序所需额外的空间复杂度为 O(\log n)。

方法二:快排思想
我们可以借鉴快速排序的思想。我们知道快排的划分函数每次执行完后都能将数组分成两个部分,小于等于分界值 pivot 的元素的都会被放到数组的左边,大于的都会被放到数组的右边,然后返回分界值的下标。与快速排序不同的是,快速排序会根据分界值的下标递归处理划分的两侧,而这里我们只处理划分的一边

我们定义函数 randomized_selected(arr, l, r, k) 表示划分数组 arr 的 [l,r] 部分,使前 k 小的数在数组的左侧,在函数里我们调用快排的划分函数,假设划分函数返回的下标是 pos(表示分界值 pivot 最终在数组中的位置),即 pivot 是数组中第 pos - l + 1 小的数,那么一共会有三种情况:

如果 pos - l + 1 == k,表示 pivot 就是第 k 小的数,直接返回即可;

如果 pos - l + 1 < k,表示第 k 小的数在 pivot 的右侧,因此递归调用 randomized_selected(arr, pos + 1, r, k - (pos - l + 1));

如果 pos - l + 1 > k,表示第 k 小的数在 pivot 的左侧,递归调用 randomized_selected(arr, l, pos - 1, k)。

参考leetcode原题

剑指 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]
示例 2:

输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]

二叉堆:优先队列
给定一长度为 N 的无序数组,其中位数的计算方法:首先对数组执行排序(使用 O ( N log ⁡ N ) O(N \log N) O(NlogN) 时间),然后返回中间元素即可(使用 O ( 1 ) O(1) O(1) 时间)。

针对本题,根据以上思路,可以将数据流保存在一个列表中,并在添加元素时 保持数组有序 。此方法的时间复杂度为 O ( N ) O(N) O(N) ,其中包括: 查找元素插入位置 O ( log ⁡ N ) O(\log N) O(logN)(二分查找)、向数组某位置插入元素 O ( N ) O(N) O(N) (插入位置之后的元素都需要向后移动一位)。

借助 heap 可进一步优化时间复杂度。

建立一个 小顶堆 A大顶堆 B ,各保存列表的一半元素,且规定:

A 保存 较大 的一半,长度为 N 2 \frac{N}{2} 2N( N 为偶数)或 N + 1 2 \frac{N+1}{2} 2N+1( N 为奇数);
B 保存 较小 的一半,长度为 N 2 \frac{N}{2} 2N( N 为偶数)或 N − 1 2 \frac{N-1}{2} 2N1( N 为奇数);
随后,中位数可仅根据 A, B 的堆顶元素计算得到。
在这里插入图片描述
算法流程:
设元素总数为 N = m + n ,其中 m 和 n 分别为 A 和 B 中的元素个数。

addNum(num) 函数

  1. m = n m = n m=n(即 N偶数):需向 A 添加一个元素。实现方法:将新元素 num 插入至B ,再将 B 堆顶元素插入至 A
  2. m ≠ n m \ne n m=n (即 N 为 奇数):需向 B 添加一个元素。实现方法:将新元素 num 插入至 A ,再将 A 堆顶元素插入至 B

这样设计的原因:添加数据前,最小堆与最大堆长度相等,加后则不相等,这里我们是往最小堆A = maxHeap加的元素,则加完后A = maxHeap多一个元素

findMedian() 函数

  1. m = n m = n m=n( N 为 偶数):则中位数为 ( A 的堆顶元素 + B 的堆顶元素 )/2
  2. m ≠ n m \ne n m=n( N 为 奇数):则中位数为 A 的堆顶元素
class MedianFinder {
public:
    // 最大堆,存储左边一半的数据,堆顶为最大值
    priority_queue<int, vector<int>, less<int>> maxHeap;
    // 最小堆, 存储右边一半的数据,堆顶为最小值
    priority_queue<int, vector<int>, greater<int>> minHeap;
    /** initialize your data structure here. */
    MedianFinder() {
    }

    // 维持堆数据平衡,并保证左边堆的最大值小于或等于右边堆的最小值
    void addNum(int num) {
        // 添加数据前,最小堆与最大堆长度相等,加后则不相等
        // 这里我们是往最小堆 maxHeap加的元素,则加完后maxHeap多一个元素
        if (minHeap.size()==maxHeap.size()){
            maxHeap.emplace(num);
            int tmp = maxHeap.top();
            maxHeap.pop();
            minHeap.emplace(tmp);
        }else{
            minHeap.emplace(num);
            int tmp = minHeap.top();
            minHeap.pop();
            maxHeap.emplace(tmp);
        }
    }
    double findMedian() {
        if (minHeap.size()==maxHeap.size()){
            return (minHeap.top()+maxHeap.top())*1.0/2;
        }else{
            return minHeap.top()*1.0;
        }
    }
};

复杂度分析:
时间复杂度:
查找中位数 O ( 1 ) O(1) O(1) : 获取堆顶元素使用 O(1)O(1) 时间;
添加数字 O ( log ⁡ N ) O(\log N) O(logN) : 堆的插入和弹出操作使用 O ( log ⁡ N ) O(\log N) O(logN) 时间。
空间复杂度 O ( N ) O(N) O(N) : 其中 N N N 为数据流中的元素数量,小顶堆 A 和大顶堆 B 最多同时保存 N 个元素。

剑指 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 。

 

限制:

0 <= 树的结点个数 <= 10000

我的思路:
声明
一个int max_d的变量,用于记录最大的左右子节点的深度差
一个dfs函数,返回当前节点的深度。

bool isBalanced(TreeNode* root) {
        if (root == nullptr) return true;
        int max_d = INT_MIN;
        dfs(root, max_d);
        return max_d > 1? false : true;
    }
int dfs(TreeNode* root, int& max_d)
    {
        if (root == nullptr) return 0;
        if (max_d>1) return 0; // 当深度差大于1时,不用计算,直接返回就行
        int depth_l = dfs(root->left, max_d);
        int depth_r = dfs(root->right, max_d);
        max_d = max(max_d, abs(depth_r - depth_l));
        return max(depth_l, depth_r)+1;
    }

复杂度分析

时间复杂度:O(n),其中 n 是二叉树中的节点个数。使用自底向上的递归,每个节点的计算高度和判断是否平衡都只需要处理一次,最坏情况下需要遍历二叉树中的所有节点,因此时间复杂度是 O(n)。

空间复杂度:O(n),其中 n 是二叉树中的节点个数。空间复杂度主要取决于递归调用的层数,递归调用的层数不会超过 n。

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

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

示例 1:

输入: n = 3
输出: 6
示例 2:

输入: n = 9
输出: 45
 

限制:

1 <= n <= 10000

我的思路:递归

int sumNums(int n) {
        if (n==0) return 0;
        return sumNums(n-1)+n;
    }

复杂度:O(n), O(n)
方法二:快速乘
考虑 A 和 B 两数相乘的时候我们如何利用加法和位运算来模拟,其实就是将 B 二进制展开,如果 B 的二进制表示下第 i 位为 1,那么这一位对最后结果的贡献就是 A ∗ ( 1 < < i ) A*(1<<i) A(1<<i) ,即 A < < i A<<i A<<i。我们遍历 B 二进制展开下的每一位,将所有贡献累加起来就是最后的答案,这个方法也被称作「俄罗斯农民乘法」,感兴趣的读者可以自行网上搜索相关资料。这个方法经常被用于两数相乘取模的场景,如果两数相乘已经超过数据范围,但取模后不会超过,我们就可以利用这个方法来拆位取模计算贡献,保证每次运算都在数据范围内。

由于计算机底层设计的原因,做加法往往比乘法快的多,因此将乘法转换为加法计算将会大大提高乘法运算的速度

除此之外,当我们计算 a ∗ b a ∗ b ab 的时候,往往较大的数计算 a ∗ b a ∗ b ab 会超出 long long int 的范围,这个时候使用快速乘法方法也能解决上述问题。
快速乘法的原理:利用乘法分配率来将 a ∗ b a ∗ b ab转化为多个式子相加的形式求解

例如:
20 × 14 = ( 10100 ) 2 × ( 1110 ) 2 = 10100 × ( 1000 + 100 + 10 ) = 10100000 + 1010000 + 101000 = ( 280 ) 10 ​ 20 × 14 \hspace{50cm} \\=(10100)_2 × (1110)_2 \hspace{50cm} \\= 10100 × ( 1000 + 100 + 10) \hspace{50cm} \\= 10100000 + 1010000 + 101000 \hspace{50cm} \\= (280)_{10} \hspace{50cm} ​ 20×14(10100)2×(1110)2=10100×(1000+100+10)=10100000+1010000+101000=(280)10

快速乘的代码如下:

    int quickMultiply(int a, int b){
        int sum=0;
        while (b){	// 只要乘数不等于0
            if (b & 1){ // 取最后一位
                sum += a;
            }
            b >>= 1; // 右移乘数
            a <<= 1; // 左移被乘数
        }
        return sum;
    }

回到本题,由等差数列求和公式我们可以知道 1 + 2 + ⋯ + n 1 + 2 + \cdots + n 1+2++n 等价于 n ( n + 1 ) 2 \frac{n(n+1)}{2} 2n(n+1),对于除以 2 我们可以用右移操作符来模拟,那么等式变成了 n ( n + 1 ) > > 1 n(n+1)>>1 n(n+1)>>1,剩下不符合题目要求的部分即为 n ( n + 1 ) n(n+1) n(n+1),根据上文提及的快速乘,我们可以将两个数相乘用加法和位运算来模拟,但是可以看到上面的 C++ 实现里我们还是需要循环语句,有没有办法去掉这个循环语句呢?答案是有的,那就是自己手动展开,因为题目数据范围 n 为 [1,10000],所以 n 二进制展开最多不会超过 14 位,我们手动展开 14 层代替循环即可,至此满足了题目的要求,具体实现可以参考下面给出的代码。

class Solution {
public:
    int sumNums(int n) {
        int ans = 0, A = n, B = n + 1;

        (B & 1) && (ans += A);
        A <<= 1;
        B >>= 1;

      // 中间省略12个相同的代码

        (B & 1) && (ans += A);
        A <<= 1;
        B >>= 1;

        return ans >> 1;
    }
};

复杂度分析

时间复杂度: O ( log ⁡ n ) O(\log n) O(logn)。快速乘需要的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)
空间复杂度:O(1)。只需要常数空间存放若干变量。

方法三:计算内存,秀操作

class Solution {
public:
    int sumNums(int n) {
        bool a[n][n+1];
        return sizeof(a)>>1;
    }
};
//ans=1+2+3+...+n
//   =(1+n)*n/2
//   =sizeof(bool a[n][n+1])/2
//   =sizeof(a)>>1

剑指 Offer 68 - I. 二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
在这里插入图片描述

示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
 

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。

我的思路:递归
相信函数的功能,例如本题,我们先交换p, q的位置,规定p->val < q->val
然后dfs函数的功能是,返回最大公共祖先,把所有情况列出来即可

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (!root) return nullptr;
        if (p->val < q->val){
            p = p; q = q;
        }else{
            TreeNode* tmp = p;
            p = q; q = tmp;
        }
        return dfs(root, p, q);
    }
    TreeNode* dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
        if (!root) return nullptr;
        if (root->left == p && root->right == q) return root;
        if (root == p && root->right == q) return root;
        if (root == q && root->left == p) return root;
        if (root->val < p->val) { // 当root小于p时,说明祖先在右边
            return dfs(root->right, p, q);
        }
        if (root->val > q->val) { // 当root大于p时,说明祖先在左边
            return dfs(root->left, p, q);
        }
        return root;
    }

剑指 Offer 68 - II. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]

在这里插入图片描述

示例 1:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:

输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
 

说明:

所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。

我的思路:迭代
int dfs函数 返回当前结点的下能够找到的p, q数目
bool flag的作用:记录第一次出现count=2的时刻,表明是最近公共祖先

int count = 0;
TreeNode* res;
bool flag = false;
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        dfs(root, p, q);
        return res;
    }

    int dfs(TreeNode* root, TreeNode* p, TreeNode* q){
        if (!root) return 0;
        if (root==q || root==p) 
        	count = dfs(root->left, p, q) + dfs(root->right, p, q) + 1;
        else count = dfs(root->left, p, q) + dfs(root->right, p, q);
        if (count == 2 && !flag) {
            flag = true;
            res = root;
        }
        return count;
    }

剑指 Offer 07. 重建二叉树

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

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

示例 1:


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

Input: preorder = [-1], inorder = [-1]
Output: [-1]
 

限制:

0 <= 节点个数 <= 5000

我不会
方法一:递归
思路

对于任意一颗树而言,前序遍历的形式总是

[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是

[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,对上述形式中的所有左右括号进行定位。

这样以来,我们就知道了左子树的前序遍历和中序遍历结果,以及右子树的前序遍历和中序遍历结果,我们就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。

细节

在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,我们就只需要 O(1) 的时间对根节点进行定位了。

下面的代码给出了详细的注释。

class Solution {
private:
    unordered_map<int, int> index;

public:
    TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
        if (preorder_left > preorder_right) {
            return nullptr;
        }
        // 前序遍历中的第一个节点就是根节点
        int preorder_root = preorder_left;
        // 在中序遍历中定位根节点
        int inorder_root = index[preorder[preorder_root]];
        
        // 先把根节点建立出来
        TreeNode* root = new TreeNode(preorder[preorder_root]);
        // 得到左子树中的节点数目
        int size_left_subtree = inorder_root - inorder_left;
        // 递归地构造左子树,并连接到根节点
        // 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
        root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
        // 递归地构造右子树,并连接到根节点
        // 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
        root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        // 构造哈希映射,帮助我们快速定位根节点
        for (int i = 0; i < n; ++i) {
            index[inorder[i]] = i;
        }
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }
};

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

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

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000
示例 2:

输入:x = 2.10000, n = 3
输出:9.26100
示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2-2 = 1/22 = 1/4 = 0.25
 

提示:

− 100.0 < x < 100.0 -100.0 < x < 100.0 100.0<x<100.0
− 2 31 < = n < = 2 31 − 1 -2^{31} <= n <= 2^{31}-1 231<=n<=2311
− 1 0 4 < = x n < = 1 0 4 -10^4 <= x_n <= 10^4 104<=xn<=104
我的方法:递归

    double positivePow(double x, int n){
        if (n==0) return 1;
        double xHalf= positivePow(x, n/2);
        if (n%2){
            return xHalf*xHalf*x;
        } else{
            return xHalf*xHalf;
        }
    }
    double myPow(double x, int n) {
        return n>=0? positivePow(x, n) : 1.0/positivePow(x, n);
    }

复杂度:O(logn), O(logn)
方法二:快速幂

位运算

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

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

提示:

请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用 二进制补码 记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:

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

输入:n = 128 (控制台输入 00000000000000000000000010000000)
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:

输入:n = 4294967293 (控制台输入 11111111111111111111111111111101,部分语言中 n = -3)
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
 

提示:

输入必须是长度为 32 的 二进制串 。

我的方法:n&(n-1)去除二进制最后一个1

int hammingWeight(uint32_t n) {
        int res=0;
        while (n){
            n = n&(n-1);
            ++res;
        }
        return res;
    }

剑指 Offer 65. 不用加减乘除做加法

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

示例:

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

提示:

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

解题思路:
本题考察对位运算的灵活使用,即使用位运算实现加法。
设两数字的二进制形式 a , b a, b a,b ,其求和 s = a + b s = a + b s=a+b a ( i ) a(i) a(i) 代表 a a a 的二进制第 i 位,则分为以下四种情况:

a(i)b(i)无进位和 n(i) (⊕异或运算)进位 c(i+1) (&运算)
0000
0110
1010
1101

观察发现,无进位和异或运算 规律相同,进位 运算 规律相同(并需左移一位)。因此,无进位和 n 与进位 c 的计算公式如下;
{ n = a ⊕ b 非 进 位 和 : 异 或 运 算 c = a & b < < 1 进 位 : 与 运 算 + 左 移 一 位 \begin{cases} n = a \oplus b & 非进位和:异或运算 \\ c = a \& b << 1 & 进位:与运算 + 左移一位 \end{cases} {n=abc=a&b<<1+

(和 s =(非进位和 n )+(进位 c )。即可将 s = a + b 转化为:
s = a + b ⇒ s = n + c s = a + b \Rightarrow s = n + c s=a+bs=n+c
循环求 n 和 c ,直至进位 c=0 ;此时 s=n ,返回 n 即可。

Q : 若数字 a 和 b 中有负数,则变成了减法,如何处理?
A : 在计算机系统中,数值一律用 补码 来表示和存储。补码的优势: 加法、减法可以统一处理(CPU只有加法器)。因此,以上方法 同时适用于正数和负数的加法 。

复杂度分析:
时间复杂度 O(1) : 最差情况下(例如 a= 0x7fffffff , b=1 时),需循环 32 次,使用 O(1) 时间;每轮中的常数次位操作使用 O(1) 时间。
空间复杂度 O(1) : 使用常数大小的额外空间。

int add(int a, int b) {
        if (b == 0) {
            return a;
        }
        
        // 转换成非进位和 + 进位
         //C++中负数不支持左移位,因为结果是不定的, 故强制为无符号数
        return add(a ^ b, (unsigned int)(a & b) << 1);
    }

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

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

示例 1:

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

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]
 

限制:

2 <= nums.length <= 10000

解题思路:分组异或
背景知识:如果一个数组中,仅存在一个数出现一次,其他的数都出现了两次,则我们将他们所有值&起来,得到的就是这个数。

本体需要用到异或来做的。
题目又要求了:时间复杂度为 O ( n ) O(n) O(n)),空间复杂度为 O ( 1 ) O(1) O(1)
因此不能用 map(空间复杂度为 O ( n ) O(n) O(n))与双重循环嵌套(空间复杂度为 O ( n 2 ) O(n^2) O(n2))。

由于数组中存在着两个数字不重复的情况,我们将所有的数字异或操作起来,最终得到的结果是这两个数字的异或结果:(相同的两个数字相互异或,值为0)) 最后结果一定不为0,因为有两个数字不重复。

演示:

4 ^ 1 ^ 4 ^ 6 => 1 ^ 6

6 对应的二进制: 110
1 对应的二进制: 001
1 ^ 6  二进制: 111

此时我们无法通过 111(二进制),去获得 110 和 001。
那么当我们可以把数组分为两组进行异或,那么就可以知道是哪

本题nums中有两个不同的数,故我们想半分将他们分到单独的组:

  1. 重复的数字进行分组,很简单,只需要有一个统一的规则,就可以把相同的数字分到同一组了。例如:奇偶分组。因为重复的数字,数值都是一样的,所以一定会分到同一组!
  2. 此时的难点在于,对两个不同数字的分组。
    此时我们要找到一个操作,让两个数字进行这个操作后,分为两组。
    我们最容易想到的就是 & 1 操作, 当我们对奇偶分组时,容易地想到 & 1,即用于判断最后一位二进制是否为 1。来辨别奇偶。

通过 & 运算来判断一位数字不同即可分为两组,那么我们随便两个不同的数字至少也有一位不同吧!我们只需要找出那位不同的数字mask,即可完成分组( & mask )操作。

由于两个数异或的结果就是两个数数位不同结果的直观表现,所以我们可以通过异或后的结果去找 mask!
所有的可行 mask 个数,都与异或后1的位数有关。

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int ret = 0;
        for (int n : nums) // 最终ret等于两个不同的值A,B的异或结果
            ret ^= n; 
        int div = 1;  
        while ((div & ret) == 0)
            div <<= 1; // 通过移动这个div到ret的第一个不为0的位置上(该位置上A,B不等)
        int a = 0, b = 0;
        for (int n : nums)
            if (div & n) // 找到与div相& 相异的两个数 即存在不同的两个数组
                a ^= n;
            else
                b ^= n;
        return vector<int>{a, b};
    }
};

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

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

示例 1:

输入:nums = [3,4,3,3]
输出:4
示例 2:

输入:nums = [9,1,7,9,7,9,7]
输出:1
 

限制:

1 <= nums.length <= 10000
1 <= nums[i] < 2^31

在这里插入图片描述

解题思路:
如下图所示,考虑数字的二进制形式,对于出现三次的数字,各 二进制位 出现的次数都是 3 的倍数。
因此,统计所有数字的各二进制位中 1 的出现次数,并对 3 求余,结果则为只出现一次的数字。

    def singleNumber(self, nums: List[int]) -> int:
        res = [0]*31
        ans = 0
        for i in range(31):
            for it in nums:
                res[i] += (it>>i)&1
            res[i] %= 3
            ans += res[i] * (1<<i)
        return ans

6065. 按位与结果大于零的最长组合

对数组 nums 执行 按位与 相当于对数组 nums 中的所有整数执行 按位与 。

例如,对 nums = [1, 5, 3] 来说,按位与等于 1 & 5 & 3 = 1 。
同样,对 nums = [7] 而言,按位与等于 7 。
给你一个正整数数组 candidates 。计算 candidates 中的数字每种组合下 按位与 的结果。 candidates 中的每个数字在每种组合中只能使用 一次 。

返回按位与结果大于 0 的 最长 组合的长度。

示例 1:

输入:candidates = [16,17,71,62,12,24,14]
输出:4
解释:组合 [16,17,62,24] 的按位与结果是 16 & 17 & 62 & 24 = 16 > 0 。
组合长度是 4 。
可以证明不存在按位与结果大于 0 且长度大于 4 的组合。
注意,符合长度最大的组合可能不止一种。
例如,组合 [62,12,24,14] 的按位与结果是 62 & 12 & 24 & 14 = 8 > 0 。
示例 2:

输入:candidates = [8,8]
输出:2
解释:最长组合是 [8,8] ,按位与结果 8 & 8 = 8 > 0 。
组合长度是 2 ,所以返回 2 。
 

提示:

1 <= candidates.length <= 10^5
1 <= candidates[i] <= 10^7

方法:位运算
因为candidates[i] <= 10^7,换算成2进制,则不超过28位,对每一位进行与1运算

int largestCombination(vector<int>& a) {
	int ret = 0;
	for (int i = 28; i >= 0; --i) {
		int cnt = 0;
		for (int j : a) {
			if (j >> i & 1) {
				++cnt;
			}
		}
		ret = max(ret, cnt);
	}
	return ret;
}

时间复杂度:O(28*n)

数学

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

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

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

示例 1:

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

限制:

1 <= 数组长度 <= 50000

摩尔投票法
因为本题是出现次数超过一半的数字,那么它有一定是众数
如果我们把众数记为 +1,把其他数记为 -1,将它们全部加起来,显然和大于 0,从结果本身我们可以看出众数比其他数多。

int majorityElement(vector<int>& nums) {
        int target, count=0;
        for (int i=0; i<nums.size(); ++i){
            if (count==0) {
                target = nums[i];
                count++;
            }else if (target==nums[i]) count++;
            else if (target!=nums[i]) count--; 
        }
        return target;
    }

复杂度分析

时间复杂度:O(n)。Boyer-Moore 算法只对数组进行了一次遍历。

空间复杂度:O(1)。Boyer-Moore 算法只需要常数级别的额外空间。

剑指 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]
 

提示:

所有元素乘积之和不会溢出 32 位整数
a.length <= 100000

解题思路:
本题的难点在于 不能使用除法 ,即需要 只用乘法 生成数组 B 。根据题目对 B[i] 的定义,可列表格,如下图所示。

根据表格的主对角线(全为 1 ),可将表格分为 上三角 和 下三角 两部分。分别迭代计算下三角和上三角两部分的乘积,即可 不使用除法 就获得结果。
在这里插入图片描述
算法流程:
初始化:数组 BB ,其中 B[0] = 1 ;辅助变量 tmp = 1 ;
计算 B[i] 的 下三角 各元素的乘积,直接乘入 B[i] ;
计算 B[i] 的 上三角 各元素的乘积,记为 tmp ,并乘入 B[i] ;
返回 B 。

class Solution {
public:
    vector<int> constructArr(vector<int>& a) {
        int len = a.size();
        if(len == 0) return {};
        vector<int> b(len, 1);
        b[0] = 1;
        int tmp = 1;
        for(int i = 1; i < len; i++) { // 我们的b[n]是要算到a[n-1]的
            b[i] = b[i - 1] * a[i - 1];
        }
        for(int i = len - 2; i >= 0; i--) {// 我们的b[0]是要算到a[1]的
            tmp *= a[i + 1];
            b[i] *= tmp;
        }
        return b;
    }
};

复杂度分析:
时间复杂度 O(N) : 其中 N 为数组长度,两轮遍历数组 a ,使用 O(N) 时间。
空间复杂度 O(1) : 变量 tmp 使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑)。

剑指 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
示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:

2 <= n <= 58

思路:数学求导
设将绳子按照 x 长度等分为 a 段,即 n = a x n = ax n=ax ,则乘积为 x a x^a xa 。观察以下公式,由于 nn 为常数,因此当 x 1 x x x^{\frac{1}{x}}x xx1x 取最大值时, 乘积达到最大值。
x a = x n x = ( x 1 x ) n x^a = x^{\frac{n}{x}} = (x^{\frac{1}{x}})^n xa=xxn=(xx1)n

    int cuttingRope(int n) {
        if (n==2) return 1;
        if (n==3) return 2;
        int m = n/3;
        int mod = n%3;
        if (mod==2) return pow(3, m)*mod;
        if (mod==0) return pow(3, m);
        else return pow(3, m-1)*2*2;
    }

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

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

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

示例 1:

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

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

限制:

1 <= target <= 10^5

滑动窗口法
对于子串问题,我们首先就应该想到滑动窗口法。
在这里,我们以l=1表示左指针,r=2表示右指针。当指针范围内的和sum=(l+r)*(r-l+1)/2大于target时,r左移;sum=(l+r)*(r-l+1)/2小于target时,l左移;sum=(l+r)*(r-l+1)/2等于target时,获得当前的子串,同时r左移。

vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> res;
        int l=1, r=2;
        while (l<r){
            int sum = (l+r)*(r-l+1)/2;
            if (sum == target) {
                vector<int> tmp;
                for (int i=l; i<=r; ++i) tmp.emplace_back(i);
                res.emplace_back(tmp);
                tmp.clear();
                ++r;
            }else if (sum<target) ++r;
            else ++l;
        }
        return res;
    }

剑指 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
示例 2:

输入: n = 10, m = 17
输出: 2
 

限制:

1 <= n <= 10^5
1 <= m <= 10^6

方法一:数学 + 递归
题目中的要求可以表述为:给定一个长度为 n 的序列,每次向后数 m 个元素并删除,那么最终留下的是第几个元素?

这个问题很难快速给出答案。但是同时也要看到,这个问题似乎有拆分为较小子问题的潜质:如果我们知道对于一个长度 n - 1 的序列,留下的是第几个元素,那么我们就可以由此计算出长度为 n 的序列的答案。

我们将上述问题建模为函数 f(n, m),该函数的返回值为最终留下的元素的序号

首先,长度为 n 的序列会先删除第 m % n 个元素,然后剩下一个长度为 n - 1 的序列。那么,我们可以递归地求解 f(n - 1, m), 就可以知道对于剩下的 n - 1 个元素,最终会留下第几个元素,我们设答案为 x = f(n - 1, m)

由于我们删除了第 m % n 个元素,将序列的长度变为 n - 1。当我们知道了 f(n - 1, m) 对应的答案 x 之后,我们也就可以知道,长度为 n 的序列最后一个删除的元素,应当是从 m % n 开始数的第 x 个元素。因此有 f(n, m) = (m % n + x) % n = (m + x) % n

int lastRemaining(int n, int m) {
        if (n==1) return 0;
        return (lastRemaining(n-1, m) + m)%n;
    }

复杂度分析

  • 时间复杂度:O(n),需要求解的函数值有 n 个。
  • 空间复杂度:O(n),函数的递归深度为 n,需要使用 O(n) 的栈空间。

方法二:迭代

我们可以得到状态转移方程:
f ( n , m ) = { f ( n − 1 , m ) + m } % n f(n, m) = \{f(n-1, m) + m\}\%n f(n,m)={f(n1,m)+m}%n

int lastRemaining(int n, int m) {
        if (n==1) return 0;
        int dp0 = 0, dp1 = 0;
        for (int i=2; i<=n; ++i){
            dp0 = dp1;
            dp1 = (dp0 + m)%i; // 注意此处不是%n,而是i
        }
        return dp1;
    }

复杂度分析

  • 时间复杂度:O(n),需要求解的函数值有 n 个。
  • 空间复杂度:O(1),只使用常数个变量。

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

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

示例 1:

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

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]
 

限制:

0 <= matrix.length <= 100
0 <= matrix[i].length <= 100

我的方法:dfs ❌,超时
当matrix较大时,例如1万个数,那么栈会非常的深
出栈入栈会非常耗时。

vector<int> res;
    int rule=0;
    int count;
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if (matrix.size()==0) return res;
        count = matrix.size()*matrix.front().size();
        vector<vector<bool>> visted(matrix.size(), vector<bool>(matrix.back().size(), false));
        dfs(matrix, visted, 0, 0);
        return res;
    }
    bool inArea(vector<vector<int>> matrix, int x, int y){
        return (0<=x && 0<=y && x<matrix.size() && y<matrix.back().size());
    }
    void dfs(vector<vector<int>>& matrix, vector<vector<bool>>& visted, int x, int y){
        if (!inArea(matrix, x, y) || visted[x][y]) 
        {
            rule = (rule+1)%4;
            return;
        }
        visted[x][y] = true;
        res.emplace_back(matrix[x][y]);
        count--;
        if (rule==0 && count>0) dfs(matrix, visted, x, y+1);
        if (rule==1 && count>0) dfs(matrix, visted, x+1, y);
        if (rule==2 && count>0) dfs(matrix, visted, x, y-1);
        if (rule==3 && count>0) dfs(matrix, visted, x-1, y);
        if (rule==0 && count>0) dfs(matrix, visted, x, y+1);
        return;
     }

方法二:指针+边界
int left_rule = -1, up_rule = 0, right_rule = n, down_rule = m;表示上下左右移动时的边界。
每次碰壁,边界都会往矩阵里缩一次。count用来统计移动的次数,当count=0后,则跳出循环。

vector<int> res;
    vector<int> spiralOrder(vector<vector<int>>& matrix) {
        if (matrix.size()==0) return {};
        int m = matrix.size(), n = matrix.back().size();
        int left_rule = -1, up_rule = 0, right_rule = n, down_rule = m;
        int i=0, j=0;
        int count = m*n;
        while (count>0){
            while (j<right_rule && count>0){
                res.emplace_back(matrix[i][j]);
                ++j; --count;
            }
            --j; ++i;
            right_rule--;
            while (i<down_rule && count>0) {
                res.emplace_back(matrix[i][j]);
                ++i; --count;
            }
            --i; --j;
            down_rule--;
            while (j>left_rule && count>0){
                res.emplace_back(matrix[i][j]);
                --j; --count;
            }
            ++j; --i;
            left_rule++;
            while (i>up_rule && count>0) {
                res.emplace_back(matrix[i][j]);
                --i; --count;
            }
            ++i; ++j;
            up_rule++;
        }
        return res;
    }

复杂度 O(n), O(1)

字符串

剑指 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) 。

我的方法:一步一步ac。。。

int strToInt(string str) {
		int start = 0, res = 0; // start为第一个'+' '-' or 数字出现的位置
		for (int i = 0; i < str.size(); ++i) {
			if (str[i] != ' ') {
				start = i;
				break;
			};
		}
		if (start == -1) return 0;

		vector<int> v;	// 记录数值
		int negtive = 1; // 记录正负号
		for (int i = start; i < str.size(); ++i) {
			int number = str[i] - '0';
			if (i == start ) { //第一个字符的正负号
				if (str[start] == '-') {
					negtive = -1;
					continue;
				}else if (str[start] == '+') continue; 
			}
			if (number >= 0 && number < 10) {
				v.push_back(number);
			} else { // 出现不合法字符
				if (v.size() == 0) return 0;
				break; 
			}
		}
		string s_cmp; // 用来记录最大最下界的字符串
		if (negtive == 1) s_cmp = to_string(INT_MAX);
		else { // 剔除符号位
			s_cmp = to_string(INT_MIN);
			s_cmp.erase(0, 1);
		}
		for (int i_reverse = 0; i_reverse < v.size() ; ++i_reverse) {
			int i = v.size() - i_reverse - 1; // i:从前往后数;i_reverse:后往前数
			if (i_reverse == s_cmp.size()-1) { // 当抵达边界长度时
				bool flag_equal = true; 
				for (auto j = 0; j <= i_reverse; j++) {// 从高位往地位遍历
					if (v[i + j] > s_cmp[j] - '0') // 只要比边界大,则返回边界
						return negtive == 1 ? INT_MAX : INT_MIN;
					else if (v[i + j] < s_cmp[j]-'0') { 
						flag_equal = false; // 如果比边界小,则记录二者不等
						break;
					}
				}
				if (flag_equal) // 二者相等,返回边界
					return negtive == 1 ? INT_MAX : INT_MIN;
			}
			else if (i_reverse > s_cmp.size() - 1) { // 当v的位数高于边界
				if (v[i]>0) // 只要不是0,则返回边界
					return negtive == 1 ? INT_MAX : INT_MIN;
			}
			res = res + v[i] * pow(10, i_reverse); // 边界内的计算
		}
		return negtive * res;
	}

复杂度:O(n), O(n)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值