DAY1
数组在内从空间的地址是连续的。
二维数组在地址空间上时连续的。
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
------------------------------------------------------------------------------------------------------------------
erase时间复杂度O(n)
数组的元素是不能删除的,只能覆盖。相当于删除一个元素,就要将后面每一个元素向前进行覆盖
双指针法:快指针指向新数组所需要的元素。慢指针 新数组的下标值。
外层for循环完成快指针移动的操作。快指针寻找到新数组所需要的元素之后,要给新数组对应的下标(慢指针)进行赋值。
当快指针所指向的元素不等于所查找的元素时,进行赋值。赋值后慢指针也应向后移动一个位置,即slow++。
1、二分
注意点:1.mid的初始化,int mid = l+(r-l)/2;
2.第一次更新边界时 r = mid -1的原因;
3.在整个数组访问完后未找到才return -1;
4.容器的定义,vector<数据类型>容器名;
5.闭区间写法时间复杂度O(log n),空间复杂度O(1);
6.在C++中,将容器(如vector<int>)作为函数参数时,如果不使用引用传递,容器会被拷贝一份传给函数。对于大型容器,这种拷贝操作可能非常昂贵,既消耗大量内存也影响程序执行效率。通过使用引用传递,函数内部使用的将是原始容器的引用,而不是其副本。这样,无论容器有多大,传递的成本都是恒定的,大大提高了效率。 在二分查找的参数列表中的vector<int>& nums意味着nums是对原始vector的引用。这样做可以避免了将整个vector复制给函数,特别是当vector很大时,这种效率的提高尤为明显;不需要额外的内存来存储容器的副本,这对于内存使用较敏感的应用尤其重要;直接作用于原始数据,如果函数内部需要修改容器,这些修改会直接反映到原始容器上。虽然在二分查找的场景下不需要修改容器,但这一点在其他使用引用传递的情景下非常重要。
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
2、删除元素
方法一:暴力解法
erase时间复杂度O(n)
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for (int i = 0; i < size; i++) {
if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位
for (int j = i + 1; j < size; j++) {
nums[j - 1] = nums[j];
}
i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--; // 此时数组的大小-1
}
}
return size;
}
};
方法二:双指针法
先定义一个快指针、慢指针。快指针寻找新数组的元素,慢指针指向更新新数组下标的位置
class Solution{
public:
int removEelement(vector<int>&nums,int val)
{
int slow = 0;
for(int fast = 0;fast<nums.size();fast++)
{
if(nums[fast]!=val)
{
nums[slow] = nums[fast];
slow++;
}
}
return slow;
}
DAY2
sort(A.begin(), A.end()); // 快速排序
vector<int> result(A.size(), 0);
用于创建一个名为 result
的整数型向量(vector
),其元素数量与另一个向量 A
的大小相同,并且所有元素的初始值都设定为 0。
for(int i = 0,j = nums.size()-1;i<=j; ) 此处为i<=j而非i<j,因为如果是i<j,即在i=j时循环退出,但是会将i=j处的元素漏掉。
只有取了最大值的操作后,才做i++或j---的操作。所以该部分不写在循环内。
-----------------------------------------------------------------------------------------------
当终止位置固定,集合内元素>=s时,收集该长度后,起始位置就可以向后移动,来缩小目前集合的长度,来看下一个集合是否符合要求
result当前的值和收集的subl取一个最小值,这样result才能持续更新总和>=s中的最小长度。
result初始为最大值int result = INT32_MAX;
sum表示当前滑动窗口里所有元素的集合,起始位置向后移动之后,sum应将目前nums[i]减去,即sum = sum - nums[i];此时,i++,q起始位置向后移动一位
由于起始位置i是持续向后移动的过程,直至滑动窗口内的值小于s停止,所以循环用while
1.有序数组的平方(双指针)
方法一:暴力解法
直接取平方后使用sort排序,时间复杂度O(n+logn)。
class Solution{
public:
vector<int> sortedsquares(vector<int>& nums){
int size = nums.size()-1;
for(int i = 0;i<size;i++){
nums[i]*=nums[i];
}
sort(nums.begin(),nums.end());
return nums;
}
};
方法二:双指针法
两个指针向中间移动,将平方较大的结果幅值给新数组
class Solution{
public:
vector<int> sortedsquares(vector<int>&nums){
vector<int>result(nums.size(),0);//创建新数组,其大小与nums相同,所有元素初始值都设为0
int k = nums.size()-1;//新数组的最后一位索引
for(int i = 0,j = nums.size()-1;i<=j; ){
if(nums[i] * nums[i] < nums[j] * nums[j]){
result[k--] = nums[j] * nums[j];
j--;}
else{
result[k--] = nums[i] * nums[i];
i++;
}
}
return result;
};
2.长度最小的子数组
滑动窗口:
for循环内的j指向滑动窗口的终止位置
result是最终取的最小的长度
sum表示当前滑动窗口内所有元素的集合
起始位置向后移动之后,需要将起始位置的值减去
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int result = INT32_MAX;
int sum = 0;//数组之和
int subl = 0;//滑动窗口的长度
int i = 0;//滑动窗口的起始位置
for(int j = 0;j<nums.size();j++)
{
sum = sum+nums[j];
while(sum>=target)
{
//取子序列的长度
subl = j-i+1;
result = result<subl?result:subl;
sum = sum-nums[i];
i++;
}
}
//如果result没有被赋值的话返回0,表示未找到长度大于target的序列
return result==INT32_MAX?0:result;
}
};
//每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
INT32_MAX
是一个在 C++ 中定义的常量,代表 32 位整型可以表示的最大正数。它是在 <climits>
或 <limits.h>
头文件中定义的,具体数值是 2,147,483,647,这个值是因为 32 位整型(int
类型)使用其中一个位来表示正负号,所以其正数范围是从 0 到 2^31 - 1。
在很多算法和函数实现中,INT32_MAX
常被用作一个初始的比较值,特别是当你需要找到一个最小值但事先不知道具体的数值范围时。通过将变量初始化为 INT32_MAX
,你可以保证任何合理的、比这个值小的数在比较时都会被识别和更新。
例如,在寻找一个数组中的最小元素时,如果你将比较变量初始化为 INT32_MAX
,则可以确保任何数组元素都会小于这个初始值,从而在第一次比较时就将变量更新为那个较小的值。相同的逻辑也适用于你的滑动窗口问题,其中你寻找的是长度最小的子数组,所以开始时将结果设为最大可能的整数值可以简化逻辑和编码过程。
DAY3
1、螺旋矩阵
给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。示例:输入: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ]
循环不变量:每循环一次转一圈,不变量即对每条边的处理规则。
左闭右开,不处理最后一个节点,留给下一条边处理。
while(loop--)//结果不为0时执行循环体,每次循环处理矩阵的一圈,当loop为0时循环结束。
i
和j
的赋值i = startx; j = starty;
是在每一圈的循环开始时进行的。这样做的目的是确保在每一圈循环开始时,i
和j
都被设置为当前圈的起始位置。这个赋值操作确保了每一圈循环都从正确的位置开始填充矩阵。因为在每一圈循环中,i
和j
会随着循环的进行而改变,因此需要在循环开始时将它们重置为当前圈的起始位置,以确保填充矩阵的正确性。
class Solution{
public:
vector<vector<int>> generateMatrix(int n){
vector<vector<int>> res(n,vector<int>(n,0));
int startx = 0;
int starty = 0;
int loop = n/2;
int count = 1;
int i ,j;
int offset = 1;
while(loop--){
i = startx;
j = starty;
for(j;j<n-offset;j++){
res[i][j] = count++;
}
for(i;i<n-offset;i++){
res[i][j] = count++;
}
for(j;j>starty;j--){
res[i][j] = count++;
}
for(i;i>startx;i--){
res[i][j] = count++;
}
startx++;
starty++;
offset++;
}
if(n%2){
res[n/2][n/2] = count;
}
}
-----------------------------------------------------------------------------------------------------------------------------
第一章总结:
数组是存放在连续内存空间上相同数据元素的集合。
数组的元素不能删除,只能覆盖。
--------------------------------------------------------------------------------------------------------------------------------
2、移除链表元素
链表中的节点 在内存中不是连续分布的,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
//单链表
struct ListNode{
int val;//节点上存储的元素
ListNode * next;//指向下一个节点的指针
ListNode(int x):val(x),next(Null){};//节点的构造函数
}
//初始化节点
ListNode * head = new ListNode(5);
//如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!
方法一:原链表删除元素
移除头结点是持续移除的过程,使用while而不能是if。
遍历链表时,要定义一个临时的指针遍历链表。
方法二:使用虚拟头结点
while(cur->next!=NULL) 判断cur的next是否为空,非空的话一直遍历链表
DAY4
1、设计链表
2、翻转链表
双指针解法,注意在移动指针时,先移动pre后移动cur。
class Solution{
pubilc:
ListNode * reverseList(ListNode * head){
ListNode * pre = nullptr;
Listnode * cur = head;
ListNode * tmp ;
while(cur!=nullptr){
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
delete cur;
delete tmp;
return pre;
};
DAY5
1、两两交换链表中的节点
利用虚拟头结点,cur指向dummyhead
循环条件一定要先为判断cur->next!=null再判断cur->next->next!=null,如果顺序反过来则会空指针异常。偶数判断cur->next为空,奇数则为cur->next->next为空。
2、删除链表的倒数第N个节点
双指针(快慢指针起始都指向虚拟头结点),快指针移动n个后,再移动一位。此时快慢指针同时移动,直至快指针到null。
此时,slow的next即是需要删除的元素。
DAY6
1、链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
思路:求出两个链表的长度,并求出两个链表长度的差值。然后移动长链表的cur,使cura和curb再同一起点(末尾对齐)。遍历cura和curb遇到相同则直接返回。
curA = headA;
和 curB = headB;
这两行代码的作用是重置 curA
和 curB
指针到各自链表的头节点。这是必要的步骤,因为在此之前的代码中,这两个指针被用于遍历各自的链表以计算链表的长度,导致它们移动到了链表的末端。
2、环形链表
DAY7
1、有效的字母异位词
2、两个数组的交集
DAY8
1、快乐数
2、两数之和
我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
std::unordered_map 底层实现为哈希表
map目的用来存放我们访问过的元素。
map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};