给定一个 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
备忘录用于记录当前位置不能通过的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
代替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;
}
地上有一个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) 的额外空间。
给你二叉树的根节点 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>& ans, vector 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 是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“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 + 直接修改节点指向
本文解法基于性质:二叉搜索树的中序遍历为 递增序列 。
将 二叉搜索树 转换成一个 “排序的循环双向链表” ,其中包含三个要素:
pre
和当前节点 cur
,不仅应构建 pre->right = cur
,也应构建 cur->left = pre
。head
和尾节点 tail
,则应构建 head->left = tail
和 tail->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个结点
给定一棵二叉搜索树,请找出其中第 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) 大小的栈空间。
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)。
给你链表的头结点 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) 时间复杂度和常数级空间复杂度下,对链表进行排序吗?
思路:归并
dfs
分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)
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 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;
}
输入整数数组 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原题
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[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} 2N−1( N 为奇数);
随后,中位数可仅根据 A, B 的堆顶元素
计算得到。
算法流程:
设元素总数为 N = m + n
,其中 m 和 n 分别为 A 和 B 中的元素个数。
addNum(num) 函数
:
N
为 偶数):需向 A
添加一个元素。实现方法:将新元素 num
插入至B
,再将 B
堆顶元素插入至 A
;B
添加一个元素。实现方法:将新元素 num
插入至 A
,再将 A
堆顶元素插入至 B
;这样设计的原因:添加数据前,最小堆与最大堆长度相等,加后则不相等,这里我们是往最小堆A = maxHeap加的元素,则加完后A = maxHeap多一个元素
findMedian() 函数
:
( A 的堆顶元素 + B 的堆顶元素 )/2
。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 个元素。
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过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。
求 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<A∗(1<<i) ,即 A < < i A<A<<i。我们遍历 B 二进制展开下的每一位,将所有贡献累加起来就是最后的答案,这个方法也被称作「俄罗斯农民乘法」,感兴趣的读者可以自行网上搜索相关资料。这个方法经常被用于两数相乘取模的场景,如果两数相乘已经超过数据范围,但取模后不会超过,我们就可以利用这个方法来拆位取模计算贡献,保证每次运算都在数据范围内。
由于计算机底层设计的原因,做加法往往比乘法快的多,因此将乘法转换为加法计算将会大大提高乘法运算的速度
除此之外,当我们计算 a ∗ b a ∗ b a∗b 的时候,往往较大的数计算 a ∗ b a ∗ b a∗b 会超出 long long int 的范围,这个时候使用快速乘法方法也能解决上述问题。
快速乘法的原理:利用乘法分配率来将 a ∗ b a ∗ b a∗b转化为多个式子相加的形式求解
例如:
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
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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;
}
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 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;
}
输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
示例 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);
}
};
实现 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<=231−1
− 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)
方法二:快速幂
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘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;
}
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
示例:
输入: 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) (&运算) |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
观察发现,无进位和 与 异或运算 规律相同,进位 和 与 运算 规律相同(并需左移一位)。因此,无进位和 n 与进位 c 的计算公式如下;
{ n = a ⊕ b 非 进 位 和 : 异 或 运 算 c = a & b < < 1 进 位 : 与 运 算 + 左 移 一 位 \begin{cases} n = a \oplus b & 非进位和:异或运算 \\ c = a \& b << 1 & 进位:与运算 + 左移一位 \end{cases} {n=a⊕bc=a&b<<1非进位和:异或运算进位:与运算+左移一位
(和 s =(非进位和 n )+(进位 c )。即可将 s = a + b 转化为:
s = a + b ⇒ s = n + c s = a + b \Rightarrow s = n + c s=a+b⇒s=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);
}
一个整型数组 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
中有两个不同的数,故我们想半分将他们分到单独的组:
通过 & 运算来判断一位数字不同即可分为两组,那么我们随便两个不同的数字至少也有一位不同吧!我们只需要找出那位不同的数字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};
}
};
在一个数组 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
对数组 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)
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
示例 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 算法只需要常数级别的额外空间。
给定一个数组 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 作为返回值,不计入复杂度考虑)。
给你一根长度为 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;
}
输入一个正整数 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;
}
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;
}
复杂度分析
方法二:迭代
我们可以得到状态转移方程:
f ( n , m ) = { f ( n − 1 , m ) + m } % n f(n, m) = \{f(n-1, m) + m\}\%n f(n,m)={f(n−1,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;
}
复杂度分析
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例 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)
写一个函数 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)