前言:
2020/1/14:今年过年前的任务就是回顾之前的刷过题,然后弄清以前没有弄懂的知识点,最后归纳整理形成自己的体系。因为“学而不思则罔,思而不学则殆”,在比赛或者面试做题时,只有三种情况,会让你不能ak所有题目,这三种情况分别是:sb失误、不会做、状态不佳。然而写总结所要做的是就是将之前的sb失误减小为0,之前不会做的题做熟做透,摸清套路,至于状态嘛,睡好觉兴奋点就是发挥出真正水平呀。
2020/1/18:经过四天的复习,终于写完了,事实证明我太混了,二刷这些题应该蛮快的,然而我比较划水和喜欢偷懒,四天才整理完,对不起,我有罪!
2020/1/19总结:做了30+道链表题,发现链表题并不是很难,很多都有使用双指针技巧法,其他大部分都是模拟题,按题目意思编写代码即可。然后你就会发现,链表中最重要的是断链
和接链
。
链表的定义(来自维基百科):
在计算机科学中,链表作为一种基础的数据结构可以用来生成其它类型的数据结构。链表通常由一连串节点组成,每个节点包含任意的实例数据(data fields)和一或两个用来指向上一个/或下一个节点的位置的链接(“links”)。
链表与顺序表的区别(来自维基百科):
链表是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表的优缺点(来自维基百科):
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的访问往往要在不同的排列顺序中转换。而链表是一种自我指示数据类型,因为它包含指向另一个相同类型的数据的指针(链接)。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
链表的类型(来自维基百科):
单向链表,双向链表以及循环链表。
单向链表的定义:
单向链表是链表类型中最为简单的一种,每个节点包含两个域,一个数据域(data),一个指针域(next)。单向链表的next节点指向下一节点,而尾节点的next节点为空。
单向链表节点的定义:
struct ListNode
{
int val;
ListNode *next;
ListNode(int x):val(x),next(nullptr){}
};
#include
using namespace std;
class MyListForward
{
private:
struct ListNode
{
int val;
ListNode *next;
ListNode(int x):val(x),next(nullptr){}
};
ListNode* head;
public:
MyListForward():head(nullptr){}
//1、获得链表中第index个节点的值
int get(int index){
int i=0;
ListNode *p=head;
while(p&&i<index){
p=p->next;
i++;
}
if(p)return p->val;
else return -1;
}
//2、在链表头部插一个值为val的节点
void addAtHead(int val){
ListNode *p=new ListNode(val);
p->next=head;
head=p;//更换头节点
}
//3、在链表尾部添加一个值为val的节点
void addAtTail(int val){
ListNode *p=new ListNode(val);
//链表为空,直接将新节点作为头节点
if(head==nullptr){
head=p;
return;
}
ListNode *q=head;
//遍历直到q的next节点为空
while(q->next){
q=q->next;
}
q->next=p;
}
//4、在索引为index的节点之前添加值为val的节点
void addAtIndex(int index,int val){
ListNode *node=new ListNode(val);
//1、index小于等于0,直接在头部插入节点
if(index<=0)
{//若index小于等于0,我们仅需要在头节点前面插入新节点就行了
//注意这里不能使用指针p,因为p=node时,p所指向的地址发生了变化,head指向的地址没有变化,所以我们这里要使用指针head
node->next=head;
head=node;
return;
}
int i=0;
ListNode *p=head;
//在索引为index的节点之前插入新节点,我们需要找到它的前驱节点,然后插入在它的前驱节点后面
while(p&&i<index-1)
{
p=p->next;
++i;
}
//2、p为索引节点的前驱节点
if(p)
{
node->next=p->next;
p->next=node;
}
}
//5、删除索引为index的节点
void deleteAtIndex(int index){
//1、index为0,我们直接删除head节点
if(index==0&&head!=nullptr)
{
ListNode *del=head;
head=head->next;
delete del;
return;
}
int i=0;
ListNode* p=head;
//删除索引为index的节点,我们需要找到它的前驱节点p,p->next为需要删除节点
while(p&&i<index-1)
{
p=p->next;
i++;
}
//2、index超过链表范围,删除失败
if(!p)return;
//3、index的位置合法,我们找到需要删除的p->next节点
if(p->next)
{
ListNode *del=p->next;
p->next=del->next;
delete del;
}
}
//6、链表长度
int length(){
int i=0;
ListNode *p=head;
while(p){
i++;
p=p->next;
}
return i;
}
//7、清空链表
void clear(){
ListNode *del=nullptr;
while(head){
del=head;
head=head->next;
delete del;
}
}
};
int main()
{
MyListForward mlf;
mlf.addAtIndex(0,10);
mlf.addAtIndex(0,20);
mlf.addAtIndex(1,30);
for(int i=0;i<mlf.length();++i){
cout<<mlf.get(i)<<" ";
}
cout<<endl;
mlf.clear();
cout<<mlf.length()<<endl;
system("pause");
}
双向链表的定义:
双向链表是一种更为复杂的链表,每个节点包含三个域,一个数据域(data)和两个指针域(prev、next),其中数据域存放节点的数值,prev指向该节点的前一个节点,next指向该节点的后一个节点。
双向链表节点的定义:
struct ListNode
{
int val;
ListNode *next,*prev;
ListNode(int x):val(x),next(nullptr),prev(nullptr){}
};
#include
using namespace std;
class MyList
{
private:
struct ListNode
{
int val;
ListNode *next,*prev;
ListNode(int x):val(x),next(nullptr),prev(nullptr){}
};
private:
//头节点尾节点都为空,表示为空链表
ListNode *head,*tail;
int size=0;
public:
MyList():size(0),head(nullptr),tail(nullptr){}
//1、获得索引为index的节点值
int get(int index){
int i=0;
ListNode *p=head;
while(p&&i<index){
p=p->next;
i++;
}
if(p)return p->val;
else return -1;
}
//2、在头部插入值为val的新节点
void addAtHead(int val){
if(head!=nullptr){
ListNode *node=new ListNode(val);
node->next=head;
head->prev=node;
head=node;
}
else{
head=new ListNode(val);
tail=head;
}
++size;
}
//3、在尾部插入值为val的新节点
void addAtTail(int val){
if(tail!=nullptr){
ListNode *node=new ListNode(val);
node->prev=tail;
tail->next=node;
tail=node;
}
else{//尾节点为空,那么头节点也为空,然后首尾节点都为新节点
tail=new ListNode(val);
head=tail;
}
++size;
}
//4、在index之前添加值为val的新节点
void addAtIndex(int index,int val){
//首先排除三种特殊情况的index,然后剩下来的index肯定在链表内
if(index<=0){
addAtHead(val);
return;
}
if(index==size){
addAtTail(val);
return;
}
if(index>size)return;
ListNode *p=nullptr,*cur=head;
int i=0;
while(cur&&i<index){
p=cur;
cur=cur->next;
i++;
}
ListNode *node=new ListNode(val);
//由于前面已经将特殊情况的index排除了,现在的p和cur都有效,都在链表内
p->next=node;
node->prev=p;
node->next=cur;
cur->prev=node;
size++;
}
//5、删除索引为index的节点
void deleteAtIndex(int index){
//链表为空时,不能删除
if(!head)return;
if(index==0)
{
ListNode *del=head;
head=head->next;
if(head){//链表有2个以上节点
head->prev=nullptr;
}
else{//链表只有一个节点,将尾部制空
tail=nullptr;
}
delete del;
size--;
return;
}
//index为最后为尾节点,我们需要删除尾节点
if(index==size-1){
ListNode *del=tail;
tail=tail->prev;
//注意这里不用处理tail为空,因为tail为空的话,那么链表只有单个节点
//然而单个节点只能删除0号节点,只有index为0时才能删除,前面已经处理过了index为0的情况了,所以这里不在处理
if(tail){
tail->next=nullptr;
}
delete del;
size--;
return;
}
int i=0;
ListNode *p=nullptr,*cur=head;
while(cur){
if(i==index){
ListNode *del=cur;
p->next=cur->next;
if(cur->next){
cur->next->prev=p;
}
delete del;
size--;
return;
}
p=cur;
cur=cur->next;
++i;
}
}
//6、获得链表的长度
int length(){
return size;
}
//7、清空链表
void clear(){
for(int i=size-1;i>=0;--i){
deleteAtIndex(i);
}
}
};
int main()
{
MyList ml;
ml.addAtHead(1);
ml.addAtTail(3);
ml.addAtHead(4);
ml.addAtHead(5);
ml.addAtIndex(1,2);
for(int i=0;i<ml.length();++i){
cout<<ml.get(i)<<" ";
}
cout<<endl;
ml.deleteAtIndex(1);
for(int i=0;i<ml.length();++i){
cout<<ml.get(i)<<" ";
}
cout<<endl;
ml.clear();
cout<<ml.length()<<endl;
system("pause");
}
环形链表的定义:
循环链表中, 首节点和末节点被连接在一起。这种方式在单向和双向链表中皆可实现。要转换一个循环链表,你开始于任意一个节点然后沿着列表的任一方向直到返回开始的节点。再来看另一种方法,循环链表可以被视为“无头无尾”。这种列表很利于节约数据存储缓存, 假定你在一个列表中有一个对象并且希望所有其他对象迭代在一个非特殊的排列下。
说明:
关于环形链表的基本代码实现,我就不写了,因为用到的也比较少,大家有兴趣可以在单链表或者双链表的基础上将首尾节点连接在一起就好了。
19. 删除链表的倒数第N个节点:快慢指针法
,起始快指针走n步后,若此时快指针已为空,表示我们删除第一个节点,直接返回head->next即可;否则此时快慢指针一起走,也就是慢指针走size-n步到达倒数第N个节点的前驱节点,快指针会到达链表的尾节点,此时我们删除slow->next节点即可。
86. 分隔链表:双指针法
,before_head链表存放比x小的节点,after_head链表存放比x大于或等于的节点,我们分别用before和after来前面两个链表添加节点,用head来遍历原始链表。当原始链表遍历完成时,我们需要将before_head链表连接上after_head链表,即before->next=after_head->next;after->next=nullptr;
。
92. 反转链表 II:双指针法
,指针pre指针指向m的前驱节点,用来将cur的next节点插入到pre后面,指针cur指向位置m起始节点,该节点保持不变,每次需要将cur连接上nxt后边的部分。换句话说,我们要将[m+1,n]的节点每次都要插到位置m之前,这样就完成了反转。
141. 环形链表:快慢指针法
,若存在环最终快慢指针会相遇;若不存在环,那么快指针一定会先走到链表尾部。
142. 环形链表 II:快慢指针法
,首先利用141的代码判断是否存在环,然后相遇之后将fast指针指向头节点,然后fast和slow现在开始每次走一步,相遇点就是环的入口。
143. 重排链表:首尾指针法
,首先将原始链表的每一个节点存放在一个数组中,然后我们取首尾指针向中间遍历,每次循环我们需要将左指针的节点连上右指针的节点,在节点连上之后,我们需要将右指针连上未排序的首节点。
160. 相交链表:本题也属于快慢指针法
,具体思路更抽象的说就是将链表AB分别变成了A->B和B->A(这里假设A更短一点),这里我们用ha遍历A->B,hb遍历B->A,然后ha指针先走到A-B的B部分,hb指针还在走在B-A的B部分,当hb走到B-A的A部分时,最终二者会相遇,而相遇部分就是公共部分。
109. 有序链表转换二叉搜索树:快慢指针法
,用两个指针,一块一慢,快的每次走两步,慢的每次走一步,这样当快指针遍历结束时,慢指针指向的也就是链表的中间位置。这时候把中间位置的节点的值作为二叉搜索树根节点的值。因为二叉搜索树对应的就是一个有序数组,根节点对应的元素值为为有序数组最中间的位置。
206. 反转链表:双指针法
,指针pre用来表示前驱节点,指针cur用来遍历链表,每次循环改变将pre->cur
的方向改变为pre<-cur
,直到遍历结束。
234. 回文链表:快慢指针法
,快指针走两步,慢指针走一步,找到链表的中点。然后,翻转后半部分。最后从前半部分链表和后半部分链表是否相同。
876. 链表的中间结点:快慢指针法
,快指针走两步,慢指针走一步,等快指针走到链表尾节点时,慢指针的位置就是中位数的位置。
147. 对链表进行插入排序:插入排序
,我们每次用head->next来进行插入排序,每次插入排序,我们需要从链表的头部开始寻找插入点,所以我们使用一个指针pre来寻找插入点,若pre->next的节点值大于等于head->next的节点时,我们的插入位置就是pre->next,这里处理起来可能麻烦点,大家仔细看代码即可。
148. 排序链表:归并排序
,先2个2个的 merge,完成一趟后,再 4个4个的 merge,直到结束。
2. 两数相加:模拟题
,由于链表是逆序存放数字的,所以链表数字从左至右低位对低位,高位对高位,因此我们从左至右遍历两个链表模拟加法运算即可,注意向高位进位。
21. 合并两个有序链表:模拟题
,每次循环比较l1->val
和l2->val
,若l1->val
,则在cur后面添加l1
;否则在cur后面添加l2
。
23. 合并K个排序链表:方法1:分治法
,将k个链表利用二分分为k个独立子链表,然后两两进行合并,最后形成一个排序链表。方法2:优雅的暴力法
,利用队列queue来实现两两链表的组合,首先将队列前两个链表合并成一个,然后添加到队列的尾部,直到队列中只有一个链表时,表示k个链表已经合成了。
24. 两两交换链表中的节点:简单递归
,每次递归交换head与nxt即可,也就是完成了两两交换链表中的节点。
25. K 个一组翻转链表:分治法
,将链表按长度k进行分组,然后每次翻转长度k的链表,注意翻转了长度k的链表后新链表的尾部还要连接未翻转部分。
61. 旋转链表:模拟题
,先求出链表长度size,若k取余size为空,那么不用旋转了,直接返回head;否则将链表首尾相连形成环形链表,由于k表示尾节点移动k%size位,那么头节点移动size-k%size位。
82. 删除排序链表中的重复元素 II:模拟题
,遍历链表,若head的节点值与head的next节点值不相等,则pre指向head,也就是不重复节点;若相等,我们需要找到重复值子链表的最后一个节点,然后令pre指向head->next,同时head移动到下一个节点。
83. 删除排序链表中的重复元素:模拟题
,直接遍历链表,遇到重复值的节点删除即可。
138. 复制带随机指针的链表:模拟题
,分三步,第一步在原链表的每个节点后面拷贝出一个新的节点,第二步拷贝random,第三步断开链表。
203. 移除链表元素:模拟题
,直接遍历链表确定是否删除节点即可。
445. 两数相加 II:双栈法
,将两个链表节点值全部压入栈中,然后每次去栈顶元素进行相加,因为这样保证了低位和低位相加,不会出现错位现象。最后直到两个栈为空且进位为0为止,就表示相加完成了。
725. 分隔链表:模拟题
,首先求出链表的长度,然后根据k来求得每段链表的平均长度,顺便求出余数。由于题目要求每部分长度相差不能超过1,而且排在前面的部分长度要大于后面部分的长度,所以我们根据余数的个数,给排在前面的部分长度+1。
430. 扁平化多级双向链表:模拟题
,迭代法,遍历链表,若发现该链表存在child节点那么就将[child,tail]这段子链表插入到当前节点的后面去,然后继续遍历链表。
817. 链表组件:模拟题
,如果当前的节点在列表G中,并且下一个节点不在列表G中,我们就找到了一个组件的尾节点,将res加1。
1171. 从链表中删去总和值为零的连续节点:模拟题
,直接遍历链表进行删除和为0的连续子链表。