一、数组
704 二分查找(1)
重点
- 数组有序(如升序)且无重复元素
- 区间的定义:不变量。在二分查找的过程中,保持不变量,就是在while循环中每次处理边界的时候都要坚持我们区间的定义来操作。
二分查找区间定义一般分两种
- 左闭右闭
[left,right]
,此时注意:left==right有意义,所以在while循环中使用left<=right
。且在判断中,当nums[middle] > target
,right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1 - 左闭右开
[left, right)
,此时注意:left==right无意义,所以在while循环中使用left < right
。且在判断中,当nums[middle] > target
,right 要赋值为 middle ,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle,下一个查询区间不会去比较nums[middle]
/*704 二分查找*/
#include <iostream>
#include <vector>
using namespace std;
// 左闭右闭区间,即查找范围为[left, right],此时left==right是有意义的,即target
int search1(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) { // left==right有意义,所以是<=
int middle = (left + right) / 2;
if (nums[middle] < target)
left = middle + 1; // 右边被赋值为middle+1了
else if (nums[middle] > target)
right = middle - 1;
else
return middle;
}
return -1;
}
// 左闭右开区间,即查找范围为[left, right),此时left==right是没有意义的
int search2(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
while (left < right){ // 注意这里是<,因为left==right没意义
int middle = (left + right) / 2;
if (nums[middle] < target)
left = middle + 1;
else if (nums[middle] > target)
right = middle;
else
return middle;
}
return -1;
}
int main() {
vector<int> nums = { -1, 0, 3, 5, 9, 12 };
int t = 9;
int ans = search1(nums, t);
cout << ans << endl;
}
35 搜索插入位置(2)
二分查找关键理解
找到正确结果无非两种情况
- left != right,middle直接是target的下标
- left == right,middle=left=right是target的下标
找不到正确结果时,left一定是和right相等的,如果
- nums[middle] > target,那么target落在[left,right]的左边,即right=middle - 1,跳出while循环,要插入target的下标就是left,即未减一的right,所以应该return right + 1;
- nums[middle] < target,那么target落在[left,right]的右边,即left=middle + 1,跳出while循环,要插入target的下标就是加一后的right,即left, 所以应该return right + 1;
- 综上应该在while循环外返回
return left;
或者return right + 1;
/*35 搜索插入位置*/
#include <iostream>
#include <vector>
using namespace std;
int searchInsert(vector<int>& nums, int target) {
// 下面部分和二分查找的写法全部相同
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int middle = (left + right) / 2;
if (nums[middle] < target) {
left = middle + 1;
}
else if (nums[middle] > target) {
right = middle - 1;
}
else
return middle;
}
// 这里返回值为left或者right + 1均可
return right + 1;
}
int main() {
vector<int> nums = { 1,3};
int t = 0;
int ans = searchInsert(nums, t);
cout << ans << endl;
}
34 在排序数组中查找元素的第一个和最后一个位置(3)
二分查找的升级,原本二分查找的条件是有序且无重复元素,本题有序且有重复元素。
- 也可以用暴力求解的方式,for循环从头找起,两个变量分别记录第一次找到target的下标,和最后一次找到target的下标。但是这样没有利用有序性
- 进阶的二分查找,通过修改边界条件,来实现找到最左边的target和最右边的target。如果对于一个序列
[5,7,7,8,8,10]
而言,target=8,那么返回[3,4]为答案。- 找最左边的target:从右边逼近,即
//定义:第一个target即最左边的target;最后一个target即最右边的target
// 如果找到target,且(middle在序列最左边 或 本target是从左找起第一个target),那么就返回middle作为左target下标
if (nums[middle] == target && (middle - 1 < 0 || nums[middle - 1] != target))
return middle;
// 从右边逼近,如果target在middle左边,或者本target不是第一个target,就缩减right界
else if (nums[middle] >= target)
right = middle - 1;
else
left = middle + 1;
- 找最右边的target:从左边逼近,即
// 如果找到target,且(middle在序列最右边 或 本target是从左找起最后一个target),那么就返回middle作为左target下标
if (nums[middle] == target && (middle + 1 >= nums.size() || nums[middle + 1] != target))
return middle;
// 从左边逼近,如果target在middle右边,或者本target不是最后一个target,就缩减left界
else if (nums[middle] <= target)
left = middle + 1;
else
right = middle - 1;
- 最后在主函数里依次调用两个函数,找到最左边的target下标和最右边的target下标赋值给ans即可。
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> ans = { -1, -1 };
if (nums.size() == 0)
return ans;
ans[0] = findLeft(nums, target);
ans[1] = findRight(nums, target);
return ans;
}
27. 移除元素(快慢指针1)
快慢指针初见
快指针在for循环里进行循环
- 所指元素和val不同时,就和慢指针一起移动;
- 所指元素和val相同时,就先走一步,慢指针不动,直到所指元素和val不同时,再一起移动,
- 一起移动的同时,将快指针指的元素赋值给慢指针。
int removeElement2(vector<int>& nums, int val) {
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex++] = nums[fastIndex];
}
}
return slowIndex;
}
- 本题还可以用前后指针(自己发明的),前指针从前向后循环,当遇到和val相同的元素时,就停下;
- 后指针开始动,当后指针指向的元素和val不同时,停下
- 将后指针的元素赋值给前指针(直接覆盖),同时列表size-1
- 但是要注意后指针的大小,要大于0,否则会越界
int removeElement1(vector<int>& nums, int val) {
// b指向最后一个元素
int b = nums.size() - 1;
// number用来计数,记录删除val后数组元素的数量
int number = nums.size();
// i从前向后循环
for (int i = 0; i < nums.size(); i++) {
//遇到和val相等的就停下,观察b
if (nums[i] == val) {
//将b从后往前循环,找到第一个和val不等的元素
while (nums[b] == val && b > 0){
b--;
}
//如果还有和val不相等的元素,就把这个元素覆盖到i的位置;
//当b为0,就说明再没有能换的元素了
if (b > 0) {
nums[i] = nums[b];
b--;
}
//计数
number--;
}
}
return number;
}
26. 删除有序数组中的重复项(2)
快慢指针新见解
- 注意快慢指针什么时候同时移动的逻辑,同时移动,意味着要用快指针的指向的值修改慢指针指向的值。
- 快指针单独移动的时候,是在寻找要替换的值。
- 同时也注意快慢指针越界的问题,灵活使用while和for循环
int removeDuplicates2(vector<int>& nums) {
int n = nums.size();
if (n == 0)
return 0;
int slowIndex = 1;
int fastIndex = 1;
while (fastIndex < n) {
// 注意快慢指针同时移动的逻辑,即当快指针下一个和当前的元素不同时移动
if (nums[fastIndex] != nums[fastIndex - 1]) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
fastIndex++;
}
return slowIndex;
}
这里if (nums[fastIndex] != nums[fastIndex - 1])
因为要删除有序列表中的重复元素,那么只有当快指针指向的元素和前一个元素不同时,才进入if语句体,进行慢指针的赋值和移动。
最后返回慢指针即可
283. 移动零(3)
- 这里同样的快慢指针,和(1)很像,只不过这里的目标值只是0,将0全部覆盖的同时,记录0的个数myCount,那么前面是原来相对位置的元素,后面myCount个就全是0,再来一个for循环将后面设置为0即可。
- 如果不用快慢指针也可以,即一遍for循环,用一个index记录非0元素的下标,若不是0,将非0元素移动到index上,再index++。那么遍历完之后,index后面的位置都应该是0,直接for循环赋值即可。
void moveZeroes(vector<int>& nums) {
int slowIndex = 0;
int myCount = 0;
for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) {
if (nums[fastIndex] != 0) {
nums[slowIndex++] = nums[fastIndex];
}
else
myCount++;
}
int n = nums.size() - 1;
for (int i = 0; i < myCount; i++) {
nums[n--] = 0;
}
}
977. 有序数组的平方(双指针4)
- 最直接的思路:将每个数原地平方,然后用sort排序
vector<int> sortedSquares1(vector<int>& nums) {
for (int i = 0; i < nums.size(); i++) {
nums[i] = pow(nums[i], 2);
}
sort(nums.begin(), nums.end());
return nums;
}
- 双指针:因为包含负数且升序排列,平方后的最大值肯定在左右两端,那么左指针指向最小的负数,右指针指向最大的正数,比较平方后的数值大小
- 新new一个ans的vector,和nums同样大小,一个指针index指向0位置
- 第一步比较的谁大,index处就存谁,相应的移动指针即可。
//快慢指针
vector<int> sortedSquares2(vector<int>& nums) {
vector<int>ans(nums.size(), 0); // new一个新vector
int left = 0; // 左指针指向最左边
int right = nums.size() - 1; // 右指针指向最右边
int index = right; //这个新指针指向新数组的最大位置,因为首先比较并存储的是最大的元素
while (index >= 0) { // 当新数组没有满的时候
if (pow(nums[left],2) < pow(nums[right],2)) { // 右边大
ans[index] = pow(nums[right], 2);// 存右边
right--;
index--;
}
else if (pow(nums[left], 2) >= pow(nums[right], 2)) { // 左边大
ans[index] = pow(nums[left], 2); // 存左边
left++;
index--;
}
}
return ans;
}
理解双指针
- 双指针并不一定是快慢指针,也可以是左右(前后)指针,类似27题,从数组两头移动。
- 双指针本质是要通过两个指针来比较两个位置的数据,同时给出一种原地更新数组的方法。
209.长度最小的子数组(滑动窗口1)
- 窗口内是什么?窗口内就是满足其和>=target的长度最小的连续子数组
- 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置? 如果当前窗口的值比target大,就要移动起始位置了(窗口就该缩小了)
- 窗口的结束位置如何移动? 窗口的结束位置就是遍历数组的指针。
- 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。
// 真的滑动窗口:类似双指针,右窗口框是for循环中的变量,遍历数组;左窗口框进行条件移动,记录一个最小的窗口长度,最后返回
int minSubArrayLen2(int target, vector<int>& nums) {
int ans = INT32_MAX; // ans保存最小的窗口长度;让ans初始化为一个很大的数
int mySum = 0; // 存储滑动窗口内值的和
int left = 0; // 窗口起始框(左窗口框)
int subLength = 0; // 窗口长度[left,right]
for (int right = 0; right < nums.size(); right++) { // 窗口末尾框(右窗口框)遍历数组
mySum += nums[right]; // 若mySum还比target小,那就右框接着移动,框进来更多的数
// 这样的while循环保证了mySum >= target时的窗口长度总是当时[left,right]条件下最短的
while (mySum >= target) { // 一旦mySum>=target,那么就开始考虑ans和左框的移动
subLength = right - left + 1; // 计算窗口长度
ans = ans < subLength ? ans : subLength; // 更新最小的窗口长度并保存在ans
mySum -= nums[left++]; // 左框移动,同时mySum减去左边的值
}
}
return ans == INT32_MAX ? 0 : ans;
}
59. 螺旋矩阵 II
循环不变量、处理好边界、循环
具体以后再说
二、链表
203.移除链表元素(1)
- 结构体c++定义见另一篇的写法
- c++删除链表元素需要用到临时指针,然后将该指针删除释放空间;其他python和java不用
while (curr->next != NULL) {
if (curr->next->val == val) { // 如果当前指针的下一个值是目标值
ListNode* temp = curr->next;
curr->next = curr->next->next; // 进行删除操作
delete temp;
}
else
curr = curr->next;
}
- 因为单链表只有指向下一个元素的指针,所以要用
curr->next
来操作,那么也就要将当前指针的下一个元素的下一个元素赋值个当前指针的下一个元素,来实现删除当前指针的下一个。 - 头节点没有被指着的指针,所以用虚拟头结点更方便。
ListNode* dummyHead = new ListNode(-1); // 创建虚拟头结点方便操作
dummyHead->next = head;
707. 设计链表(2)
完成链表的设计,包括:节点设计、链表成员变量、链表构造函数、获取index处的值、头部插入节点、尾部插入节点、在index处插入节点、删除index处的节点这些操作。
- 注意在不涉及增删节点的操作,如
get(index)
:- index的范围是
index < 0 || index >= size
,因为从0开始计数,第size个没有值。 - curr指针指向的就是当前节点,即
LinkedNode* curr = dummyHead->next;
(dummyHead是虚拟头结点)
- index的范围是
- 在涉及增删节点的操作中
- 如
addAtTail(val)
、addAtIndex(int index, int val)
、deleteAtIndex(int index)
中:curr指针指向的是当前节点的上一个节点,因为为了增加或删除,必须依赖于上一个节点。即LinkedNode* curr = dummyHead;
- 如
- 涉及到index的操作:要判断,index是否可以和size相等,当可以给尾部添加节点时,index可以和size相等,如:
addAtIndex(int index, int val)
;当是要删除(尾部)节点或者取(尾部)节点时,index不可以和size相等,如:deleteAtIndex(int index)
、get(index)
- 单链表的私有成员变量有
dummyHead
和size
- 记得增删节点时,更新
size
206. 反转链表(3)
链表其实很好翻转,原地翻转就行。
翻转需要三个指针
- pre:指向前一个节点。是要在翻转后被指向的节点
- curr:指向当前节点。是要作为翻转后的发出指针的节点
- temp:临时存储下一个节点的位置。因为curr进行翻转后,丢失了下一个节点的信息。
- 迭代解法:一个while循环不断进行移动就可以,当curr指向的节点为空时,意味着pre指向了最后一个节点,那么返回pre即可。
// 迭代解法
// 为了进行翻转,有三个指针,分别指向前一个,当前,后一个元素
ListNode* reverseList1(ListNode* head) {
ListNode* pre = nullptr; // 前一个和当前元素是要进行指针翻转的元素
ListNode* curr = head;
ListNode* temp; // 后一个元素是临时保存在temp中,方便curr移动
while (curr != nullptr) {
temp = curr->next;
curr->next = pre; // 翻转操作
pre = curr;
curr = temp;
} // curr为空,意味着pre移动到了链表尾部
return pre;
}
- 递归解法:一个help函数帮助递归,入参就为进行移动的指针。当传入参数时,就实现了指针的移动。
// 递归解法, 和迭代本质一样
ListNode* reverseHelp(ListNode* pre, ListNode* curr) { // 用到一个help函数协助递归
if (curr == nullptr)
return pre;
ListNode* temp = curr->next;
curr->next = pre; // 进行翻转
reverseHelp(curr, temp); // 这里的传参实现了指针移动
}
ListNode* reverseList2(ListNode* head) {
return reverseHelp(NULL, head); // 进行初始化与递归
}
24. 两两交换链表中的节点(4)
难点主要在模拟交换的过程,判断循环的条件上。
- 模拟交换的过程:画图求解最方便。其实就是指针指向的不断变化,分清楚前后关系,存储好没有被指向的孤儿节点。
要想交换XuNi->1->2->3
变为XuNi->2->1->3
那么只能是首先XuNi->2
,然后2->1
,然后1->3
交换过程要用三个指针来辅助
那么就要做好保存节点的准备
while (curr->next && curr->next->next) { // 当curr下面有2个节点时继续。因为curr下面只有1个或没有节点都交换不了
temp1 = curr->next; // 保存1的位置
temp2 = curr->next->next->next; // 保存3的位置
然后进行交换【一定画图】
curr->next = curr->next->next; // XuNi->2
curr->next->next = temp1; // 2->1
curr->next->next->next = temp2; // 1->k
curr = curr->next->next; // 将curr顺移两个位置,进行下一次迭代
最后一行将curr顺移两个位置,那是因为curr下面的两个节点已经完成了交换,顺移两个,进行下一次迭代。
- 判断循环的条件
根据上边最后curr的分析,判断循环的条件呼之欲出,即要保证,curr下面有两个节点。没有节点或者有一个节点都不再进行交换。 - 最后返回时,记得这样返回
head = dummyHead->next; // 这里注意记得重新赋值给head
return head;
总的过程如下:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(); // 设置虚拟头结点方便
dummyHead->next = head;
// 以下三个节点用于辅助交换
ListNode* curr = dummyHead;
ListNode* temp1;
ListNode* temp2;
while (curr->next && curr->next->next) { // 当curr下面有2个节点时继续。因为curr下面只有1个或没有节点都交换不了
temp1 = curr->next; // 保存1的位置
temp2 = curr->next->next->next; // 保存k的位置
curr->next = curr->next->next; // XuNi->2
curr->next->next = temp1; // 2->1
curr->next->next->next = temp2; // 1->k
curr = curr->next->next; // 将curr顺移两个位置,进行下一次迭代
}
head = dummyHead->next; // 这里注意记得重新赋值给head
return head;
}
19. 删除链表的倒数第 N 个结点
我的思路是很直接的,就是删除这个结点。
借助一个数组
- 先将链表复制到数组里
while (curr) {
temp.push_back(curr->val);
curr = curr->next;
}
- 记录链表的长度,根据n来获得目标结点的下标
int index = temp.size() - n;
- 再从数组构建新链表,遇到目标元素时,跳过它。最终得到新的链表,即完成删除
for (int i = 0; i < temp.size(); i++) {
if (i == index) {
continue; // 跳过index的元素
}
ListNode* newTemp = new ListNode(temp[i]);
newCurr->next = newTemp;
newCurr = newCurr->next;
}
return newHead->next;
快慢指针再见
题解的思路是快慢指针
- 首先快慢指针都指向虚拟头结点
// 虚拟头结点
ListNode* dummyHead = new ListNode();
dummyHead->next = head;
// 快慢指针
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
- 快指针移动n+1个节点,为了让慢指针指向目标结点的上一个节点
// 首先移动快指针,移动n+1次,这样后面的slow指针就会指到目标指针的上一个
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
- 快慢指针同时移动,当快指针指向最后的null时停止。然后删除慢指针指向节点的下一个节点。即完成删除
// 同时移动快慢指针,直到fast指到最后的null
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 此时slow指向目标值的上一个,删除目标值
ListNode* temp = slow->next;
slow->next = slow->next->next;
delete temp;
return dummyHead->next;
全部代码
// 快慢指针法
ListNode* removeNthFromEnd2(ListNode* head, int n) {
// 虚拟头结点
ListNode* dummyHead = new ListNode();
dummyHead->next = head;
// 快慢指针
ListNode* fast = dummyHead;
ListNode* slow = dummyHead;
// 首先移动快指针,移动n+1次,这样后面的slow指针就会指到目标指针的上一个
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
// 同时移动快慢指针,直到fast指到最后的null
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 此时slow指向目标值的上一个,删除目标值
ListNode* temp = slow->next;
slow->next = slow->next->next;
delete temp;
return dummyHead->next;
}
快慢指针代码量少,但是速度好像没有我的快。
刷题待办
-
深度系统的学习STL。
-
vector
-
queue
-
map
-
priority_queue (heap)
-
-
深度系统的学习指针、面向对象、拷贝构造等(c++和python)
-
学习数论的一些基础,不管是用工具还是自己写
- 最大公约数
- 素数判断
- 取模
- 组合数学
-
深度系统的学习字符串(string库和c风格字符)
-
动态规划
-
广搜
-
深搜
-
二分查找(复习与推广)
-
图论(最小生成树、最短路径)
面试题 02.07. 链表相交
本题原本的思路是:借助两个数组,来存储地址,然后依次从A数组取元素地址去和B比较,遇到相同的就返回,但是最后一个测试点超时了。
后来题解的思路其实很直接,也和正常思维差不多。
要想找到分叉点,那么肯定分叉点一直到结尾的元素(地址)都相同,如果把两个链表末尾对齐的话,将长链表的头移到和短链表相同的位置,然后开始同时移动进行对应位置的比较,一旦相同就返回,即分叉点找到。
ListNode* getIntersectionNode2(ListNode* headA, ListNode* headB) {
ListNode* curA = headA;
ListNode* curB = headB;
int lenA = 0, lenB = 0;
while (curA != nullptr) { // 求链表A的长度
lenA++;
curA = curA->next;
}
while (curB != nullptr) { // 求链表B的长度
lenB++;
curB = curB->next;
}
curA = headA;
curB = headB;
// 让curA成为最长链表的头,lenA为他的长度
if (lenB > lenA) {
swap(lenA, lenB);
swap(curA, curB);
}
// 求长度差
int diff = lenA - lenB;
// 让A和B末尾对齐
while (diff--) {
curA = curA->next;
}
// 遍历A和B,遇到相同的就返回
while (curA != nullptr) {
if (curA == curB)
return curA;
curA = curA->next;
curB = curB->next;
}
return nullptr;
}
142. 环形链表 II
本题对快慢指针的应用以及数学知识的应用比较多,针对性较强,用来练手叭。
三、哈希表
242.有效的字母异位词
- 正儿八经的第一次将哈希表有意识的运用到算法题中,哈希表第一次接触应该是大一小学期刷题的时候,那时候觉得这个办法很不错,也是单纯的一直用的数组作为哈希表,当时把这个称为用下标保存。现在知道这个叫哈希表。第二次是数据结构吧,系统的学习了哈希表,各种哈希函数,哈希方法,这次算是要好好的实践一下。
- 本题用一个
int record[26] = {0}
作为哈希表,来存储26个字母出现的次数 - 对于参数s,遍历每一个字母,在对应下标存储出现的次数
- 对于参数t,遍历每一个字母,在对应下标进行减一的操作
- 最后检查record数组,发现有不是0的,就返回false,若全是0,那么代表是字母异位词。
bool isAnagram(string s, string t) {
int record[26] = { 0 }; // 数组哈希表
for (char c : s) {
record[c - 'a']++; // 如果s中该字符c出现了,对应下标加一
}
for (char c : t) {
record[c - 'a']--; // 如果t中该字符c也出现了,对应下标减一
}
bool flag = true;
for (int f : record) {
if (f != 0) // 检查record数组,若有一个不为0,那么就代表s和t不是字母异位词
flag = false;
}
return flag;
}
1002. 查找常用字符
本题算是哈希表比较巧妙的用法,同时也用到比较巧妙的思想。
就是维护一个hash[26]
,这个数组中存储的是所有单词中 出现的 字母的最低频次。最后按照顺序,输出一个字母,频次减一,直到0。这个hash表遍历完,也就是最终答案了。
过程如下:
- 用一个hash[26]存储第一个单词中的字母频次,用来初始化
- 用一个hashOtherStr[26]存储下一个单词中的字母频次,然后和hash作比较,取对应频次的最小值存到hash中。(完成了字符的提取,不在所有单词中出现的字母频次自然是0)
- 按照顺序输出每个字母,当频次不为0的时候。
#include<iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 哈希表
// 统计每个字符出现的频率,取最低的频率的字符输出即可
vector<string> commonChars(vector<string>& words) {
vector<string> result;
if (words.size() == 0)
return result;
int hash[26] = { 0 };
for (int i = 0; i < words[0].size(); i++) {
hash[words[0][i] - 'a']++;
}
int hashOtherStr[26] = { 0 };
for (int i = 0; i < words.size(); i++) {
for (int j = 0; j < words[i].size(); j++) {
hashOtherStr[words[i][j] - 'a']++;
}
for (int i = 0; i < 26; i++) {
hash[i] = min(hash[i], hashOtherStr[i]);
}
}
for (int i = 0; i < 26; i++) {
while (hash[i] != 0) {
string s(1, i + 'a');
result.push_back(s);
hash[i]--;
}
}
return result;
}
349. 两个数组的交集
本题思路不难,难在有效的去重然后查找。
- 首先自己想到用vector的去重方法,先sort排序,然后用下方法
nums1.erase(unique(nums1.begin(), nums1.end()), nums1.end());
- 然后用find依次去查找,查到了就存
auto t_iter = find(nums1.begin(), nums1.end(), *iter);
if (t_iter != nums1.end())
res.push_back(*t_iter);
总的代码:
#include<iostream>
#include <vector>
#include <algorithm>
#include <unordered_set>
using namespace std;
// 直接vector去重、find方法
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
nums1.erase(unique(nums1.begin(), nums1.end()), nums1.end());
nums2.erase(unique(nums2.begin(), nums2.end()), nums2.end());
auto iter = nums2.begin();
vector<int> res;
for (; iter != nums2.end(); iter++) {
auto t_iter = find(nums1.begin(), nums1.end(), *iter);
if (t_iter != nums1.end())
res.push_back(*t_iter);
}
return res;
}
题解中用到unordered_set这个容器去完成上述步骤。
- 首先该容器自带去重功能,直接用nums1去初始化容器就可
- 其次该容器支持find方法,同样的查到了就存
总的代码:
vector<int> intersection1(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> res;
unordered_set<int> temp(nums1.begin(), nums1.end());
for (auto num : nums2) {
if (temp.find(num) != temp.end()) {
res.insert(num);
}
}
return vector<int>(res.begin(), res.end());
}
202. 快乐数
本题刚开始确实没思路,想着无限循环怎么破,怎么就跳出无限循环了,怎么用到哈希表?
看了题解,突破点正是在无限循环上。
- 无限循环意味着这个
sum
和,在计算过程中可能会进入某个循环,那么就意味着false - 如果还没有进入循环,那么就表示有希望得到1
- 得到1就返回true
- 里面的哈希表作用是来查找是否有重复的sum
其中一个常用和常见的函数,对一个整数的各位进行操作
int getSum(int n) { // 这个函数要会写,很常用很常见的操作。就是取一个数的各位去操作
int sum = 0;
while(n){
int temp = n % 10; // 模10得到个位
sum += temp * temp; // 对个位进行运算
n /= 10; // 除以10删掉个位
}
return sum;
}
以下是全部代码:
int getSum(int n) { // 这个函数要会写,很常用很常见的操作。就是取一个数的各位去操作
int sum = 0;
while(n){
int temp = n % 10; // 模10得到个位
sum += temp * temp; // 对个位进行运算
n /= 10; // 除以10删掉个位
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while (true) {
int sum = getSum(n);
if (sum == 1) // 直到运行到1,才返回true
return true;
if (set.find(sum) != set.end()) { // 如果查到了,那么表示循环了,就false
return false;
}
else { // 没有查到,那么表示还没有进入循环,插入set
set.insert(sum);
}
n = sum; // 这一步记得更新n
}
}
1. 两数之和
本题居然是用哈希表。也比较巧妙,暴力应该是过不了的。
题中应当是一定能够查找出一个target对应的两个数的,那么将两数之和转化为两数之差,即
- 用一个
unordered_map<int, int>
,first存储数组中的数,second存储对应的下标 - 依次用target减去数组中的数,用这个得到的差去map中查找是否存在
- 若存在,则意味着
减数+差 = target
,返回减数的下标与差的下标即是答案 - 若不存在,就将当前数存入map来更新这个map
确实牛,下面是全部代码:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> map; // key->vector中的数,value->数对应的下标
for (int i = 0; i < nums.size(); i++) { // 遍历数组
auto iter = map.find(target - nums[i]); // 查询另一个加数在不在当前map里
if (iter != map.end()) { // 如果找到了,那么表明找到这样的两个数了
vector<int> res = { iter->second, i }; // 一个数(差)下标是iter的下标,另一个是减数的下标
return res;
}
else {
map.insert(pair<int, int>(nums[i], i)); // 没找到就要将这个数和它的下标更新进map
}
}
return {};
}
383. 赎金信
解题思路和242一致,用一个数组统计即可。
bool canConstruct(string ransomNote, string magazine) {
int hash[26] = { 0 };
for (char c : ransomNote) {
hash[c - 'a']++;
}
for (char c : magazine) {
hash[c - 'a']--;
}
for (int flag : hash) {
if (flag > 0)
return false;
}
return true;
}
我的代码和242题出奇的一致。
四、字符串
344. 反转字符串
题目很简单,调用c++的库函数reverse()会很快解决,但本题考虑的就是自己写reverse函数。所以是要自己重写。
- 反转最简单的就是借用一个中间temp,从字符串两头开始,两两交换然后向内移动即可。
这里使用一个下标,用i
表示左边待交换字符的下标,用len-i-1
的方式得到右边待交换字符的下标。
void reverseString(vector<char>& s) {
int len = s.size();
for (int i = 0; i < len / 2; i++) {
char t = s[i];
s[i] = s[len - i - 1];
s[len - i - 1] = t;
}
}
- 题解是直接使用了swap函数进行交换,同时考虑显式的表示两个下标i和j,
i=0,j=len-1
,然后同时向内移动,在i<len/2
处停下即可。
void reverseString2(vector<char>& s) {
int len = s.size();
for (int i = 0, j = len - 1; i < len / 2; i++, j--) {
swap(s[i], s[j]);
}
}
541. 反转字符串II
本题规则是稍微复杂了一点,但是总归是有规律的,用for循环来处理规律即可。
- 找每个2k区间的开头,然后反转k个字符;
- 判断最后余下字符的长度,然后进行对应规则的反转
string reverseStr(string s, int k) {
int len = s.size();
for (int i = 0; i < len / (2 * k); i++) {
reverse(s.begin() + i * 2 * k, s.begin() + i * 2 * k + k);
}
int remain = len % (2 * k);
if (remain < k) {
reverse(s.end() - remain, s.end());
}
else if (remain >= k && remain < 2 * k) {
reverse(s.end() - remain, s.end() - remain + k);
}
return s;
}
- 题解思路差不多,但是要比我的更简洁
string reverseStr(string s, int k) {
for (int i = 0; i < s.size(); i += (2 * k)) {
// 1. 每隔 2k 个字符的前 k 个字符进行反转
// 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符
if (i + k <= s.size()) {
reverse(s.begin() + i, s.begin() + i + k );
continue;
}
// 3. 剩余字符少于 k 个,则将剩余字符全部反转。
reverse(s.begin() + i, s.begin() + s.size());
}
return s;
}