一、数组
知识总结
1.关于数组,需要注意:
- 数组下标从0开始
- 数组内存空间地址连续
- 数组的元素不能删除,只能覆盖
2.数组经典题型:
(1)二分法
明确循环不变量规则,即在循环中对边界的处理要保持区间的定义不变,常见的有写法有左闭右闭[left,right]和左闭右开[left,right).
以Leetcode704二分查找为例,对于左闭右闭,思路代码可见文章代码随想录算法训练营Day 1左闭右闭.由于考研中关于区间代码基本都是左闭右闭,一上来做此题时也没有注意到这点,使用的便是左闭右闭,区别主要体现在①right赋值为nums.size()-1;②while循环条件为left<=right;③当num[mid]>target时,right=mid-1。而对于左闭右开:①right赋值为nums.size();②while循环条件为left<right;③当num[mid]>target时,right=mid。需要注意,左闭右闭和左闭右开左侧均是闭区间,故当num[mid]<target时,left均赋为mid+1。
(2)双指针法
又称快慢指针法,通过一个快指针和一个慢指针在一个循环中完成两层循环才能完成的工作。
数组中的元素为什么不能删除,主要是因为以下两点:
- 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
- C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。
(3)滑动窗口
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。以Leetcode209长度最小的子数组为例,思路代码见代码随想录算法训练营Day 2
(4)模拟行为
模拟类的题目不涉及到什么算法,就是单纯的模拟,十分考察对代码的掌控能力。在Leetcode59螺旋矩阵II中,再一次介绍到了循环不变量原则,做题时需要把握好每一种情况的边界条件。详情见代码随想录算法训练营Day 2
拓展题目
Leetcode69 x的平方根(二分法)
题目链接:x 的平方根
思路:比较简单的一个二分法的应用,本题还是根据左闭右闭进行。稍微不同之处在于,因x的平方根需要向下取整,故最终结果一定在mid*mid<或=x时出现,故将两种情况合并即可。
代码
class Solution {
public:
int mySqrt(int x) {
int left = 0;
int right = x;
int res;
while (left <= right) {
int mid = left + (right - left) / 2;
if (mid * mid <= x) {
res = mid;
left = mid + 1;
} else
right = mid - 1;
}
return res;
}
};
复杂度
时间复杂度 O(logn)
空间复杂度O(1)
Leetcode26 删除有序数组中的重复项(双指针)
题目链接:删除有序数组中的重复项
思路:从第二个元素开始利用快指针遍历一次数组,当nums[fast] != nums[fast - 1]时,用慢指针记录该元素并后移。
代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int fast = 1, slow = 1; //快慢指针,其中第一个元素必然不会被删除
for (; fast < nums.size(); fast++) {
if (nums[fast] != nums[fast - 1]) {
nums[slow++] = nums[fast];
}
}
return slow;
}
};
复杂度
时间复杂度 O(n)
空间复杂度O(1)
Leetcode904 水果成篮(滑动窗口)
题目链接:水果成篮
思路:利用滑动窗口思想①先将第一个元素放入type数组,differ自增②当果树数量大于1时,进入for循环,每一次count均自增③当differ<=2时,及时更新res④当differ==3时,超出篮子容纳范围,此时fruits[i]与fruits[i-1]必为两种不同的水果种类,当j从i-1向begin反向寻找到不等于fruits[i-1]的水果时,此时j指向的为需删除水果种类的最右一个,最后更新count为i-j以及type[0]和type[1]的内容即可。
利用哈希表可以使使复杂度降到O(n),目前还未掌握,后续补档。
代码
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int differ = 0; //某一时刻窗口的水果种类
int begin = 0; //窗口起始
int count = 1; //窗口内水果数量
int res = 1; //结果,至少为1
vector<int> type(3, -1); //用于记录窗口内水果种类的内容
type[differ++] = fruits[0];
for (int i = 1; i < fruits.size(); i++) {
count++;
if (fruits[i] != type[0] && fruits[i] != type[1]) {
type[differ++] = fruits[i];
}
if (differ <= 2) { //更新最大值
if (count > res)
res = count;
} else if (differ == 3) { //删除窗口中第一种
differ--;
type[0] = fruits[i];
type[1] = fruits[i - 1];
int j = i - 1;
for (; j >= begin; j--) {
if (fruits[j] != fruits[i - 1]) {
break;
}
}
count = i - j;
begin = j + 1;
}
}
return res;
}
};
复杂度
时间复杂度 O(n^2)
空间复杂度O(1)
Leetcode54 螺旋矩阵(模拟行为)
二、链表
知识总结
1.基础知识
- 链表是一种通过指针串联在一起的线性结构,每一个结点由两部分组成,一个是数据域,一个是指针域(存放指向下一个结点的指针),最后一个结点的指针域指向null
- 链表的种类主要为:单链表,双链表,循环链表
- 链表的存储方式:链表的节点在内存中是分散存储的,利用指针链接,故不可随机访问。
- 链表的增删改查操作实现。
2.链表结点的定义
// 单链表
struct ListNode {
int val; // 结点上存储的元素
ListNode *next; // 指向下一个结点的指针
ListNode(int x) : val(x), next(NULL) {} // 结点的构造函数
};
3.链表和数组性能的比较
数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组,所以数组的删除只是逻辑上的覆盖。因地址空间连续,数组支持随机访问。
链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。因地址空间不连续,故链表不支持随机访问
4.链表经典题目
(1)虚拟头结点
链表的一大问题就是操作当前结点必须要找前一个结点才能操作。这就造成了,每次操作首元结点都要单独处理,但是使用虚拟头结点,就可以解决这个问题,将所有结点的操作一致化。以Leetcode203移除链表元素为例,详情可见代码随想录算法训练营Day 3
(2)链表的基本操作
以Leetcode707设计链表为例,涉及了:
- 获取链表第index个结点的数值
- 在链表的最前面插入一个结点
- 在链表的最后面插入一个结点
- 在链表第index个节点前面插入一个结点
- 删除链表的第index个结点的数值
其中还是使用了虚拟头结点的做法,详情可见代码随想录算法训练营Day 3
(3)反转链表
主要有迭代法和递归法,除此之外如果使用虚拟头结点,可以利用头插法实现逆置。具体思路描述可以看文章代码随想录-反转链表。
(4)双指针(删除倒数第N个节点、链表相交、环形链表)
这三道题本人认为本质上都是快慢指针的应用,详情可见代码随想录算法训练营Day 4
三、总结
数组和链表题目是比较常见且常考的,总结这些题型利于自己形成体系,便于回顾。