算法题小结:

???? 每日一练

​470. 用 Rand7() 实现 Rand10()​

由于已经实现了rand7,rand10实现需要保证的是每个数字出现的概率相等。 rand7()*rand7() 1/46

但是要保证数字1 -10

所以 idx = rand7() + (rand7()-1)*7;

return 1 + (idx-1) %10

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

记忆化搜索。

​面试题 17.14. 最小K个数​

sort,返回。

​ 剑指 Offer 22 链表中倒数第k个节点​

两种方法:顺序遍历

定义快慢指针

​165. 比较版本号​

注意: 以,进行分割

与运算,如果其中一个版本比较短,后面部分作为0与长的继续相比

比较的两个数转为数字。

???? 剑指Offer

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

用队列实现,

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

用队列实现,同时借助一个临时vector 存储每一层的结果。

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

与上面题一样。Z字 其实判断奇数偶数层就可以完成。

​剑指 Offer 26. 树的子结构​

递归。前序遍历。

在左子树中查找子结构,在右子树查找子结构。 对于当前接地那进行比较,注意是||的关系。

比较的函数总 如果p2为空 则无论原树长什么样都匹配

如果p2不为空,p1为空 则不匹配。

在匹配的情况下 左右子树也比象相等。

​剑指 Offer 27. 二叉树的镜像​

递归。 前序遍历,用一个临时的节点作交换就可以完成。

注意这个临时节点 不该只指向根。

​剑指 Offer 04. 二维数组中的查找​

找规律右上角

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

二分查找,由于原先是递增函数 所以可以寻找 左边序列 右边序列 Notes


  • 中间值和最右边的进行比较,
  • 左右指针相等就退出
  • 相等时候 适当的缩小右边的方位 right–

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

哈希表存储 判断第一次出现。

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

方法 一 和下标进行比较并且交换。 下标与数组相等则返回。

方法二 定义一个Bool容器,将访问过的设置为True 如果下一个依然为true则直接返回下标。

方法三(不推荐 只是熟悉一下知识点) 用哈希表 以及 unmap.find() == unmap.end();

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

方法一:顺序查找

二分循环 判断条件是二分位置和下标是否相等,等于则left = mid +1 否则right = mid -1 ;最后返回left.

​#剑指 Offer 53 - I 在排序数组中查找数字 I​

这题用二分查找的方法,类似查找重复数字方位一样

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

如果前面累加的小于等于0,则直接抛弃

​剑指Offer 05.替换空格​

方法一:定义一个新的字符串,简单的判断 数组元素添加用Push_back

方法二: 需要注意字符串空间需要通过s.resize(totalLen)重新分配。 从尾开始 防止覆盖

​剑指 Offer 58 - II. 左旋转字符串​

​189. 旋转数组​

如下步骤就可以左旋转字符串:


  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

右旋转,其实就是反转的顺序改动一下,优先反转整个字符串,步骤如下:


  1. 反转整个字符串
  2. 反转区间为前k的子串
  3. 反转区间为k到末尾的子串

​剑指 Offer 06. 从尾到头打印链表​

reverse || c.rbegin() 返回一个逆序迭代器,它指向容器c的最后一个元素

​剑指 Offer 24. 反转链表​

方法一 用容器作为中转,然后赋值给新的链表。

头插法

​剑指 Offer 35. 复杂链表的复制​

用哈希表来存储 第一部分放数值

接下来是nex,random

class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
// 用一个哈希表来存储
unordered_map<Node* , Node*> unmap;
// 哈希表的第一部分放数值
for(Node* p= head; p != nullptr; p = p->next){
unmap[p] = new Node(p->val);
}
// 接下来next, random
for(Node* p= head; p != nullptr; p = p->next){
unmap[p]->next = unmap[p->next];
unmap[p]->random = unmap[p->random];
}
return unmap[head];
}
};

???? 重点面试题

二分查找

左闭右开,左闭右闭

​35. 搜索插入位置​

二分查找 mid = left + (right - left)/2 防止溢出

返回right+1的位置。

​在排序数组中查找元素的第一个和最后一个位置​

非常经典,由二分查找定义确定左右的区间位置。

    int leftIdx = searchRangeIndex(nums, target, true);
int rightIdx = searchRangeIndex(nums, target, false) - 1;

nums[mid] > target || (flag && nums[mid]>= target)

叨叨念:

本周主要还是刷剑指Offer,在LeetCode刷后发现很多新的结题思路,又二刷了一遍。

???? 周赛

​5863. 统计特殊四元组​

​5864. 游戏中弱角色的数量​

算法题Code

???? 每日一练

​470. 用 Rand7() 实现 Rand10()​

由于已经实现了rand7,rand10实现需要保证的是每个数字出现的概率相等。 rand7()*rand7() 1/46

但是要保证数字1 -10

所以 idx = rand7() + (rand7()-1)*7;

return 1 + (idx-1) %10

class Solution {
public:
int rand10() {
int row, col, idx;
do {
row = rand7();
col = rand7();
idx = col + (row - 1) * 7;
} while (idx > 40);
return 1 + (idx - 1) % 10;
}
};

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

记忆化搜索。

class Solution {
public:
int fib(int n) {
// 记忆化搜索
int fist = 1;
int second = 1;
if(n == 0) return 0;
if(n == 1 || n==2) return 1;
int now =3;
for(int i = 3; i<=n; i++){
now = (fist + second) % 1000000007;
fist = second;
second = now;
}
return now ;
}
};

​面试题 17.14. 最小K个数​

sort,返回。

class Solution {
public:
vector<int> smallestK(vector<int>& arr, int k) {
if(arr.size() == 0 || k==0){
return vector<int>{};
}
//暴力 排序
sort(arr.begin(), arr.end());
return vector<int>{arr.begin(), arr.begin()+k};
}
};

​ 剑指 Offer 22 链表中倒数第k个节点​

两种方法:顺序遍历

定义快慢指针

class Solution {
public:
ListNode* getKthFromEnd(ListNode* head, int k) {
// 第一次 遍历出链表的长度
// 第二次 将头结点移动到head
if(head == nullptr || k < 0){
return head;
}
// 遍历得出链表长度
int len = 0;
ListNode* cur = head;
while(cur!=nullptr){
len++;
cur = cur->next;
}
// 移动头指针 len -k;
for(int i=0; i< len-k && head!=nullptr; i++){
head = head->next;
}
return head;
}
};
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param pHead ListNode类
* @param k int整型
* @return ListNode类
*/
ListNode* FindKthToTail(ListNode* pHead, int k) {
// write code here
// 这道题的难点在于鲁棒性
ListNode* pback = nullptr;
if(k==0)
return pback;
if(pHead==nullptr)
return pback;
// 借助两个指针,一个先跑k-1 一个在跟着跑
ListNode* pfront = pHead;
for(int i=1; i<=k-1; i++)
{
if(pfront->next !=nullptr)
{
pfront= pfront->next;
}
else
return nullptr;
}
pback = pHead;
// 接下来后出发开始追,当先出发的指向末尾的时候开始打印
while(pfront->next !=nullptr)
{
pfront = pfront->next;
pback = pback->next;
}
return pback;
}
};

​165. 比较版本号​

注意: 以,进行分割

与运算,如果其中一个版本比较短,后面部分作为0与长的继续相比

比较的两个数转为数字。

???? 剑指Offer

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

用队列实现

class Solution {
public:
vector<int> levelOrder(TreeNode* root) {
vector<int> result;
// 递归的效率不急循环 先进先出用队列
if(root == nullptr)
return result;
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
TreeNode* tmpNode = q.front();
result.push_back(tmpNode->val);
q.pop();
if(tmpNode->left) q.push(tmpNode->left);
if(tmpNode->right) q.push(tmpNode->right);
}
return result;
}
};

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

用队列实现,同时借助一个临时vector 存储每一层的结果。

class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if(root == nullptr)
return result;
// 与不分层的区别 在遍历每一层的时候 用一个额外的容器来保存每一层的结果
queue<TreeNode*> q;
q.push(root);
while(!q.empty()){
vector<int> temp;
// 这是关键,获取每一层的大小
int size =q.size();
while(size--){
TreeNode* tempNode = q.front();
temp.push_back(tempNode->val);
q.pop();
if(tempNode->left) q.push(tempNode->left);
if(tempNode->right) q.push(tempNode->right);
}
result.push_back(temp);
}
return result;
}
};

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

与上面题一样。Z字 其实判断奇数偶数层就可以完成。

class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
// 与上面一题一样。只是如果是奇数的时候逆转。
vector<vector<int>> result;
if(root == nullptr)
return result;
// 与不分层的区别 在遍历每一层的时候 用一个额外的容器来保存每一层的结果
queue<TreeNode*> q;
q.push(root);
int num = 1;
while(!q.empty()){
vector<int> temp;
// 这是关键,获取每一层的大小
int size =q.size();
while(size--){
TreeNode* tempNode = q.front();
temp.push_back(tempNode->val);
q.pop();
if(tempNode->left) q.push(tempNode->left);
if(tempNode->right) q.push(tempNode->right);
}
if(num%2==0){
reverse(temp.begin(),temp.end());
result.push_back(temp);
}else{
result.push_back(temp);
}
num++;
}
return result;
}
};

​剑指 Offer 26. 树的子结构​

递归。前序遍历。

在左子树中查找子结构,在右子树查找子结构。 对于当前接地那进行比较,注意是||的关系。

比较的函数总 如果p2为空 则无论原树长什么样都匹配

如果p2不为空,p1为空 则不匹配。

在匹配的情况下 左右子树也比象相等。

class Solution {
public:
bool hasSubStru(TreeNode* A,TreeNode* B){
if(B == nullptr) return true;
if(A == nullptr) return false;
if(A->val == B->val)
{
return hasSubStru(A->left,B->left) && hasSubStru(A->right,B->right);
}
else
return false;
}
bool isSubStructure(TreeNode* A, TreeNode* B) {
// 这类题的话,通过栈或者队列就难以完成 需要借助递归来实现。
if(A == nullptr || B == nullptr)
return false;
// 递归的方法来做 注意前面两个是递归 第三个 才是判断判断根节点
return isSubStructure(A->left,B) ||
isSubStructure(A->right,B) ||
hasSubStru(A,B);
}
};

​剑指 Offer 27. 二叉树的镜像​

递归。 前序遍历,用一个临时的节点作交换就可以完成。

注意这个临时节点 不该只指向根。

class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
// 前序遍历遇到叶子节点结束
if(root == nullptr)
return root;
// 通过前序遍历 交换各个不为空的节点
if(root->left==nullptr && root->right == nullptr)
return root;
// 交换节点
TreeNode* temp = root->left;
root->left = root->right;
root->right = temp;
if(root->left !=nullptr)
mirrorTree(root->left);
if(root->right !=nullptr)
mirrorTree(root->right);
return root;
}
};

​剑指 Offer 04. 二维数组中的查找​

找规律右上角

class Solution {
public:
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if(matrix.size() == 0 || matrix[0].size() == 0)
return false;
// 取得右上角 然后进行限定
int cols = matrix[0].size()-1, rows = matrix.size()-1;
int col = cols,row = 0;
while(row<=rows && col>=0){
if(matrix[row][col] > target ) col--;
else if(matrix[row][col] < target) row++;
else if(matrix[row][col] == target) return true;
}
return false;
}
};

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

二分查找,由于原先是递增函数 所以可以寻找 左边序列 右边序列 Notes


  • 中间值和最右边的进行比较,
  • 左右指针相等就退出
  • 相等时候 适当的缩小右边的方位 right–

class Solution {
public:
int minArray(vector<int>& numbers) {
// 排序的问题一般都是用二分去做
int i = 0, j = numbers.size()-1;
while(i<j){
// 借助左右边的数字和中间的数值进行比较 判断旋转点的位置
int mid = i + (j-i)/2;
// 如果中间值小于右边 旋转点在左边排序的数组中
if(numbers[mid] < numbers[j]) j = mid;
else if(numbers[mid] > numbers[j]) i = mid + 1;
// 如果相等的时候 考虑特例 10111, 11101 缩进逼近的范围
else if(numbers[mid] == numbers[j]) j--;
}
return numbers[j];
}
};

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

哈希表存储 判断第一次出现。

class Solution {
public:
char firstUniqChar(string s) {
// 按顺序按照哈希表进行存储
int len = s.size()-1;
if(s.size() == 0){
return ' ';
}
unordered_map<char, int> umap;
for(int i=0; i<=len; i++){
umap[s[i]]++;
}
for(int i=0; i<=len; i++){
if(umap[s[i]] == 1)
return s[i];
}
return ' ';
}
};

​ #剑指 Offer 03 数组中重复的数字​

方法 一 和下标进行比较并且交换。 下标与数组相等则返回。

方法二 定义一个Bool容器,将访问过的设置为True 如果下一个依然为true则直接返回下标。

方法三(不推荐 只是熟悉一下知识点) 用哈希表 以及 unmap.find() == unmap.end();

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

方法一:顺序查找

方法二:二分循环 判断条件是二分位置和下标是否相等,等于则left = mid +1 否则right = mid -1 ;最后返回left.

class Solution {
public:
int missingNumber(vector<int>& nums) {
// 由题意 必然存在缺失值 缺失在0~N-1中
for(int i=0; i<nums.size();i++){
if(i!=nums[i]){
return nums[i]-1;
}
}
// 如果缺失为边界值
return nums.size();
}
};

二分法

class Solution {
public:
int missingNumber(vector<int>& nums) {
// 有序且唯一 二分。
// 循环二分 分层左边 和右边
// 查找的下标Mid,
int left =0;
int right = nums.size()-1;
while(left<=right){
// 防止溢出
int mid = left + (right - left)/2;
if(nums[mid] == mid ){
left = mid +1;
}else{
right = mid -1;
}
}
// 返回左边的末尾 右边的第一个
return left;
}
};

​#剑指 Offer 53 - I 在排序数组中查找数字 I​

这题用二分查找的方法,类似查找重复数字方位一样

class Solution {
public:
int binarySearch(vector<int>& nums, int target, bool lower) {
int left = 0, right = (int)nums.size() - 1, ans = (int)nums.size();
while (left <= right) {
int mid = (left + right) / 2;
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid;
} else {
left = mid + 1;
}
}
return ans;
}

int search(vector<int>& nums, int target) {
int leftIdx = binarySearch(nums, target, true);
int rightIdx = binarySearch(nums, target, false) - 1;
if (leftIdx <= rightIdx && rightIdx < nums.size() && nums[leftIdx] == target && nums[rightIdx] == target) {
return rightIdx - leftIdx + 1;
}
return 0;
}
};
class Solution {
public:
int search(vector<int>& nums, int target) {
// 利用二分法 如果没有则返回0 如果有 统计一下数量 因为是有序的 是可以直接加1
// 但是 无法确定位置 有重复 所以通过区间来设置左右区间相减
if(nums.empty() || nums.size() == 0 ){
return 0;
}
int len = nums.size()-1;
int left = 0, i = 0;
int right=len, j = len;
// 右边区间 第一个相等的数值
while(left <= right){
int mid = (left + right) / 2;
if(nums[mid] <= target){
left = mid + 1;
}else{
right = mid -1;
}
}
i = left;
cout<<"left"<<left<<" right "<<right;
left = 0, right = len;
// 左边区间
while(left <= right){
int mid = (left + right) / 2;
if(nums[mid] < target){
left = mid + 1;
}else{
right = mid -1;
}
}
j = right;
cout<<"left"<<left<<" right "<<right;
return i - j - 1 ;
}
};

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

如果前面累加的小于等于0,则直接抛弃

class Solution {
public:
int maxSubArray(vector<int>& nums) {
// 定义一个累加的变量,定义一个累加数组
if(nums.empty())
return 0;
int addSum = nums[0], maxSum = nums[0];
for(int i=1; i<nums.size(); ++i){
// 如果前面累加的小于等于0,则直接抛弃
if(addSum <=0)
addSum = nums[i];
else
addSum += nums[i];
if(addSum > maxSum){
maxSum = addSum;
}
}
return maxSum;
}
};

​剑指Offer 05.替换空格​

方法一:定义一个新的字符串,简单的判断 数组元素添加用Push_back

方法二: 需要注意字符串空间需要通过s.resize(totalLen)重新分配。 从尾开始 防止覆盖

class Solution {
public:
string replaceSpace(string s) { //字符数组
string array; //存储结果

for(auto &c : s){ //遍历原字符串
if(c == ' '){
array.push_back('%');
array.push_back('2');
array.push_back('0');
}
else{
array.push_back(c);
}
}
return array;
}
};

书中的方法并不适合字符串数组 因为空间是不可修改的

class Solution {
public:
string replaceSpace(string s) {
// 先做一个简单的假设,申请的字符串空间是足够的。
// 统计空格数, 倒过来计算
int spaceTotal = 0, totalLen = s.length();
int originLen = totalLen;
// cout<<totalLen;
for(int i=0; i<s.length(); ++i){
if(s[i]==' '){
spaceTotal++;
}
}
// cout<<spaceTotal<<endl;
totalLen += spaceTotal*2;
s.resize(totalLen);
// 从头到尾开始遍历
for(int i=originLen-1; i>=0 && totalLen!=i; --i){
if(s[i] == ' '){
s[--totalLen] = '0';
s[--totalLen] = '2';
s[--totalLen] = '%';
}else{
s[--totalLen] =s[i];
}
}
return s;
}
};

​剑指 Offer 58 - II. 左旋转字符串​

​189. 旋转数组​

如下步骤就可以左旋转字符串:


  1. 反转区间为前n的子串
  2. 反转区间为n到末尾的子串
  3. 反转整个字符串

右旋转,其实就是反转的顺序改动一下,优先反转整个字符串,步骤如下:


  1. 反转整个字符串
  2. 反转区间为前k的子串
  3. 反转区间为k到末尾的子串

reverse()会将区间[beg,end)

时间复杂度为O(1)

class Solution {
public:
string reverseLeftWords(string s, int n) {
// 不采用分片,时间复杂度为1的解法
// 翻转前N个
// 翻转s.begin+n+1 s.end
// 翻转整个S
// reverse()会将区间[beg,end)
// 这道题恰好给出了限制 所以 k 不大可能超出
if(s.empty() ||s.size() == 1){
return s;
}
int k = n % s.size();
reverse(s.begin(), s.begin()+k);
reverse(s.begin()+k, s.end());
reverse(s.begin(),s.end());
return s;
}
};

两倍字符串去做

class Solution {
public:
string reverseLeftWords(string s, int n) {
// 有一个技巧就是将字符串长度增加为原来的两倍
// 然后返回子字符串直接从begin+n 到 begin+n+length
int len = s.size();
if(s.empty() || s.size() == 1){
return s;
}
if(n>=len) n = n % len;
s += s;
return s.substr(n, len);
}
};

​剑指 Offer 06. 从尾到头打印链表​

reverse || c.rbegin() 返回一个逆序迭代器,它指向容器c的最后一个元素

class Solution {
public:
vector<int> reversePrint(ListNode* head) {
vector<int> result;
ListNode* cur = head;
while(cur !=nullptr){
result.push_back(cur->val);
cur = cur->next;
}
//reverse(result.begin(),result.end());
return vector<int>(result.rbegin(),result.rend());
}
};

​剑指 Offer 24. 反转链表​

方法一 用容器作为中转,然后赋值给新的链表。

class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 第一反应 用一个容器存储 然后在这个链表上修改
vector<int> result;
ListNode* cur = head;
// 当链表长度为空 或者为1的时候 直接返回头结点
if(head == nullptr || head->next == nullptr)
return head;
while(cur!=nullptr){
result.push_back(cur->val);
cur = cur->next;
}
ListNode* newHead = head;
for(int i=result.size()-1; i>=0; i--){
newHead->val = result[i];
//cout<<newHead->val<<endl;
newHead = newHead->next;
}
return head;
}
};

头插法

class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* new_head = nullptr;
while (head)
{
ListNode* next = head->next; //备份
head->next = new_head; // 更新new_head
new_head = head;//移动new_head
head = next;
}
return new_head;
}
};

​剑指 Offer 35. 复杂链表的复制​

用哈希表来存储 第一部分放数值

接下来是nex,random

class Solution {
public:
Node* copyRandomList(Node* head) {
if(head == nullptr) return nullptr;
// 用一个哈希表来存储
unordered_map<Node* , Node*> unmap;
// 哈希表的第一部分放数值
for(Node* p= head; p != nullptr; p = p->next){
unmap[p] = new Node(p->val);
}
// 接下来next, random
for(Node* p= head; p != nullptr; p = p->next){
unmap[p]->next = unmap[p->next];
unmap[p]->random = unmap[p->random];
}
return unmap[head];
}
};

???? 重点面试题

二分查找

左闭右开,左闭右闭

​35. 搜索插入位置​

二分查找 mid = left + (right - left)/2 防止溢出

返回right+1的位置。

​在排序数组中查找元素的第一个和最后一个位置​

非常经典,由二分查找定义确定左右的区间位置。

    int leftIdx = searchRangeIndex(nums, target, true);
int rightIdx = searchRangeIndex(nums, target, false) - 1;

nums[mid] > target || (flag && nums[mid]>= target)

叨叨念:

本周主要还是刷剑指Offer,在LeetCode刷后发现很多新的结题思路,又二刷了一遍。

???? 周赛

​5863. 统计特殊四元组​

class Solution {
public:
int countQuadruplets(vector<int>& nums) {
//直接用暴力
if(nums.empty())
return 0;
int count = 0;
for(int a = 0; a<nums.size(); a++){
for(int b = a+1; b<nums.size(); b++){
for(int c = b+1; c<nums.size(); c++){
for(int d = c+1; d<nums.size(); d++)
if(nums[a]+nums[b]+nums[c] == nums[d] )
count++;
}
}
}
return count;
}
};

​5864. 游戏中弱角色的数量​


没想到呀。

照攻击从大到小排序,攻击相同的按照防御从小到大排序

然后遍历数组,维护遍历过的角色的防御的最大值 \textit{maxDef}maxDef。对于当前角色 pp,如果 pp 的防御小于 \textit{maxDef}maxDef,那么说明前面有防御比 pp 高的角色(记作 qq);同时,根据上面的排序规则,如果 qq 的攻击和 pp 相同,那么 qq 的防御不会超过 pp,矛盾,因此 qq 的攻击必然大于 pp,于是 qq 的攻防均高于 pp,pp 是一个弱角色。


class Solution {
public:
int numberOfWeakCharacters(vector<vector<int>>& properties) {
int len = properties.size();
// 按照攻击从大到小排序 如果攻击相同则按照防御从小到大排序
sort(properties.begin(), properties.end(), [&](const auto &a, const auto &b){
if(a[0] != b[0])
return a[0] > b[0];
else
return a[1] < b[1];
});

int maxDef = -1;
int ans = 0;
for(int i = 0; i < len; i++){
if(properties[i][1] < maxDef ){
ans++;
}
maxDef = max(properties[i][1],maxDef);
}
return ans;
}
};