数据结构与算法-链表篇
往期内容
1-链表
2-栈与队列
3-树与图
4-哈希表
5-查找
6-排序
7-贪心
8-递归与分治
9-动态规划
链表
基本概念
线性表:是由零个或者多个元素组成的有限序列
一、线性表的存储结构
线性表的存储结构有两种:线性顺序结构和链式存储结构
1.1 顺序存储结构
//定义顺序存储线性表
#define MAXSIZE 20//定义最大的元素个数
typedef int ElemType;//定义数据的类型
struct SqList
{
ElemType data[MAXSIZE];
int length;//定义线性表的当前长度
};
1.2 链式存储结构
//单链表的存储结构
typedef int ElemType;
struct Node
{
ElemType data;//数据域
Node *Next;//指针域
};
//定义节点的指针
typedef Node* LinkList;
1.3 双向链表
typedef int ElemType;
//2.双向链表
typedef struct DulNode
{
ElemType data;
DulNode *prior;//指向前驱指针
DulNode *next;//指向后继指针
}DulNode,*DuLinkList;
二、基本操作
2.1 获取元素
顺序存储结构 (一定要注意边界条件)
/*
获取元素操作:bool GetElem(SqList L,int i,ElemType *e)
功能:在线性表L中,将第i个位置的值,传递给e
如果传递成功返回True,失败返回False
*/
bool GetElem(SqList L,int i,ElemType *e)
{
//防止越界
if(L.length==0 || L.length>MAXSIZE || i<1)
{
return false;
}
*e=L.data[i-1];
return true;
}
链式存储结构 (注意第二个判断条件)
/*
获取元素操作:bool GetElem(LinkList L,int i,ElemType *e)
功能:在线性表L中,将第i个位置的值,传递给e
如果传递成功返回True,失败返回False
*/
bool GetElem(LinkList L,int i,ElemType *e)
{
//假设存在头节点
LinkList p=L->Next;//p指向第一个节点
int j=1;
while(p && j<i)
{
p=p->Next;
++j;
}
if(!p || j>i)//越界:1.链表为空,2.i的值小于0
{
return false;
}
*e=p->data;//获取第i个位置的值
return true;
}
2.2 插入操作
顺序存储
/*
插入操作:bool ListInsert(SqList *L,int i,ElemType e)
功能:在线性表L的i位置插入数值e,并返回bool类型的标志,判断是否插入成功
假设从插入点开始,所有元素向后移动
*/
bool ListInsert(SqList *L,int i,ElemType e){
//判断是否越界
if(L->length==MAXSIZE)//判断顺序线性表是否满
{
return false;
}
if(i<1|| i>L->length+1)
{
return false;
}
if(i<L->length)//如果不在表尾,其他元素需要空出当前的位置
{
for(int k=L->length-1;k>i-1;k--)//从当前的位置k的值,依次向后移动
{
L->data[k+1]=L->data[k];//要从末尾的后一位开始,借位然后从后往前移动。错误写法:L->data[k]=L->data[k-1];
}
}
L->data[i-1]=e;
L->length++;
return true;
}
链式存储
/*
插入操作:bool ListInsert(LinkList *L,int i,ElemType e)
功能:在线性表L的i位置插入数值e,并返回bool类型的标志,判断是否插入成功
假设从插入点开始,所有元素向后移动
*/
bool ListInsert(LinkList *L,int i,ElemType e){
LinkList p,s;
p=*L;
int j=1;
while(p && j<i)
{
p=p->Next;
++j;
}
if (!p || j>i)
{
return false;
}//p指向i-1的位置
//申请内存空间
s=(LinkList)malloc(sizeof(Node));
s->data=e;
s->Next=p->Next;
p->Next=s;
return true;
}
2.3 删除操作
顺序存储 (除最后一个位置,其他位置都需要移动元素)
/*
删除操作:bool LisDelete(SqList *L,int i,ElemType *e)
功能:在L顺序线性表中的,第i个位数,删除元素,并将删除的值存储在(*e)中,并返回bool类型的标志,判断是否删除成功
*/
bool LisDelete(SqList *L,int i,ElemType *e)
{
if(L->length==0)//切记,不要忘记此条件
{
return false;
}
if(i<1 && i>L->length)
{
return false;
}
*e=L->data[i-1];
if(i<L->length)
{
for(int k=i-1;k<L->length;k++)
{
L->data[k]=L->data[k+1];
}
}
L->length--;
return false;
}
链式存储
/*
删除操作:bool LisDelete(LinkList *L,int i,ElemType *e)
功能:在L线性表中的,第i个位数,删除元素,并将删除的值存储在(*e)中,并返回bool类型的标志,判断是否删除成功
*/
bool LisDelete(LinkList *L,int i,ElemType *e)
{
LinkList p,s;
p=*L;//头节点
int j=1;
while(p && j<i)
{
p=p->Next;
j++;
}
if(!(p) || j>i)
{
return false;
}//指向第i-1个元素
s=p->Next;
p->Next=s->Next;
*e=s->data;
free(s);//回收释放内存
return true;
}
2.4 链式表的头插法与尾插法
头插法较为简单,尾插法需要额外的指针,记录尾结点
//头结点指针:数据域为空的结点:先用malloc函数生成结点,在定义next指针域为空即可,数据用不用赋值。
/*
单链表整表的创建1:头插法
随机产生n个元素的值,建立单线性链表L
创建单线性链表:void createListHead(LinkList *L,int n)
n表示线性表的长度
*/
void createListHead(LinkList *L,int n)
{
LinkList p;
int i;
//定义一个随机数种子
//srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
(*L)->Next=NULL;//先建立一个带有头节点的空单链表
for(int i=0;i<n;i++)
{
//创建节点,申请内存
p=(LinkList)malloc(sizeof(Node));
p->data=i;//输入数据
//链接各个结点即可
p->Next=(*L)->Next;//当前节点指向头节点的原来节点,相当于在头节点和原来节点之间插入了当前节点
(*L)->Next=p;//表头指向当前节点
}
}
//先使用malloc函数生成一个头,中间执行循环的结点链接,在函数的最后一句中,将尾结点的next指针指向null
/*
单链表整表的创建2:尾插法
随机产生n个元素的值,建立单线性链表L
创建单线性链表:void createListTail(LinkList *L,int n)
n表示线性表的长度
*/
void createListTail(LinkList *L,int n)
{
LinkList p,s;
int i;
//定义一个随机数种子
//srand(time(0));
*L=(LinkList)malloc(sizeof(Node));
//(*L)->Next=NULL;//先建立一个带有头节点的空单链表
p=*L;
for(int i=0;i<n;i++)
{
//创建节点,申请内存
s=(LinkList)malloc(sizeof(Node));
s->data=i;//输入数据
p->Next=s;
//s->Next=NULL;
p=s;
}
s->Next=NULL;//尾结点为空
}
2.5 链式整表删除
/*
单链表的整表删除:
*/
bool ClearList(LinkList *L)
{
LinkList p,s;
p=(*L)->Next;//指向第一个结点
//回收结点指针
while(p)
{
s=p->Next;
free(p);
p=s;
}
//回收头指针
(*L)->Next=NULL;
return true;
}
三、Leetcode刷题
2.1 构建
结构
struct ListNode{
int val;
ListNode *next;
};
简单的构建方法
ListNode a(1);
ListNode b(4);
ListNode c(6);
ListNode d(0);
ListNode e(5);
ListNode f(7);
a.next = &b;
b.next = &c;
d.next = &e;
e.next = &f;
f.next=nullptr;
ListNode *head=&a;//头结点指针
2.2 反转链表
leetcode 206-反转链表
思路:双指针,一个指向反转链的头结点 (初始化为null,正好形成兑成),另一个记录头结点的下一结点。(头指针,头指针的前位置,头指针下一位置)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
//双指针
ListNode * new_head=nullptr;
ListNode * next=nullptr;
while(head)
{
next=head->next;
head->next=new_head;
new_head=head;
head=next;
}
return new_head;
}
};
2.3 反转链表2
leetcode 92-反转链表
思路:1.注意四个位置结点的保存,2.注意返回的头结点 3.用while的时候要注意++
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode *l1=nullptr;//反转常用步骤
ListNode *l2=head;
for(int i=1;i<left;i++){
l1=l2;
l2=l2->next;
}
ListNode *r1=nullptr;//反转常用步骤
ListNode *r2=l2;
ListNode *p=nullptr;//临时next结点
for(int i=left;i<=right;i++){
p=r2->next;
r2->next=r1;
r1=r2;
r2=p;
}
if(l1==nullptr){//头结点特殊处理
head=r1;
}else{
l1->next=r1;
}
l2->next=r2;
return head;
}
};
方法2:头插法
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
ListNode dumpmyHead(-1);
dumpmyHead.next=head;
ListNode *l1=&dumpmyHead,*l2=head;
for(int i=1;i<left;i++){
l1=l2;
l2=l2->next;
}
for(int i=left;i<right;i++){
ListNode *p=l2->next;
l2->next=p->next;
p->next=l1->next;
l1->next=p;
}
return dumpmyHead.next;
}
};
2.2,2.3两题总结:反转链表的时候,需要有三个指针,分别保存当前结点前驱的地址(初始化为nullptr),当前结点地址(默认为头指针地址)和后继的地址(避免丢失),
2.4 求两个链表的交点
leetcode 160-相交链表
思路1:暴力解法,定义两个vector容器,分别遍历链表a和b,将所有的结点分别存入对应的容器中,然后从后往前遍历,知道找到不相等的结点为止,返回之前保存的结点。(注意:1.写’;’,2.用while循环里面记得写++操作,不然导致死循环)
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
vector<ListNode *> vec_A;
vector<ListNode *> vec_B;
ListNode *p=headA;
while(p)
{
vec_A.push_back(p);
p=p->next;
}
p=headB;
while(p)
{
vec_B.push_back(p);
p=p->next;
}
int i=vec_A.size()-1;
int j=vec_B.size()-1;
p=nullptr;
while(i>=0 && j>=0 && vec_A[i]==vec_B[j])
{
p=vec_A[i];
i--;
j--;
}
return p;
}
};
思路2:首先遍历A链表的所有结点,并将A链表中的所有结点保存在一个set集合中,然后遍历B中的所有结点,每次遍历的时候检查该结点是否在set集合中 ,如果在返回该结点,否则返回nullptr (注意:set的添加元素的操作是insert,并非push_back,另外set有自带find方法,vector没有自带的find方法)。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
//使用set集合,找到第一个相同的指针地址
set<ListNode *> node_set;
ListNode *pnode=headA;
while(pnode)
{
node_set.insert(pnode);
pnode=pnode->next;
}
pnode=headB;
while(pnode)
{
if(node_set.find(pnode)!=node_set.end())
{
return pnode;
}
pnode=pnode->next;
}
return nullptr;
}
};
思路三:由于求公共的部分,可以先将链表对齐,即先从较长的链表遍历到较短链表的位置,然后同步遍历,直到找到相同的公共位置。备注:空间复杂度为O(1)
class Solution {
int getListLen(ListNode *head)//求数组的长度
{
int len=0;
while(head)
{
len++;
head=head->next;
}
return len;
}
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
int lenA=getListLen(headA);
int lenB=getListLen(headB);
if(lenA>lenB)
{
for(int i=0;i<lenA-lenB;i++)
{
headA=headA->next;
}
}
else
{
for(int i=0;i<lenB-lenA;i++)
{
headB=headB->next;
}
}
while(headA && headB)
{
if(headA==headB)
return headA;
headA=headA->next;
headB=headB->next;
}
return NULL;
}
};
2.5链表求环
Leetcode-141 环形链表
Leetcode-142 环形链表2
思路1:集合的思想set,每遍历一个结点,先判断是否在集合中,如果没有将其加入到set集合中,否则是环形链表,当遍历到空结点时,返回否。
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *nodePtr=head;
set<ListNode *> nodeSet;
while(nodePtr)
{
if(nodeSet.find(nodePtr)!=nodeSet.end())//集合中是否有结点
{
return true;
}
nodeSet.insert(nodePtr);
nodePtr=nodePtr->next;
}
return false;
}
};
思路2:快慢指针,用两个指针分别指向头结点,一个走一步,另一个走两步,快指针追上慢指针,是环形链表,快指针走到空结点,不是环形链表
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *fast=head;//快
ListNode *slow=head;//慢
while(fast)
{
fast=fast->next;
slow=slow->next;
if(fast==nullptr)
{
break;
}
fast=fast->next;
if(fast==slow)
return true;
}
return false;
}
};
环形链表2
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
//快慢指针的方法,套圈的方法 a+b+c+b=2*(a+b) 因此a=c,从a和c点同时出发,相遇点是环的入口
ListNode * fast=head;
ListNode * slow=head;
ListNode * meet=nullptr;
while(fast)
{
//先走一步
fast=fast->next;
slow=slow->next;
if(!fast)
return nullptr;
fast=fast->next;//快指针走两步
if(fast==slow)
{
meet=fast;
break;//一定要记得跳出循环
}
}
while(meet && head)
{
if(meet==head)
return head;
meet=meet->next;
head=head->next;
}
return nullptr;
}
};
2.6 分割链表
Leetcode-86 分割链表
思路:新建两个空的头指针,比较每个结点的元素,使用尾插法,向两个链表中插入对应的元素,拼接数据,并将尾部指针置为nullptr,== (如果有nodePtr->next操作在等号的左边,记得备份指向的数据)==
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
ListNode small_head(0);
ListNode big_head(0);
//尾差法-尾指针
ListNode *small_end_ptr=&small_head;
ListNode *big_end_ptr=&big_head;
//移动指针
ListNode *nodePtr=head;
while(nodePtr)
{
if(nodePtr->val<x)
{
small_end_ptr->next=nodePtr;//尾插数据
small_end_ptr=nodePtr;
}
else
{
big_end_ptr->next=nodePtr;
big_end_ptr=nodePtr;
}
nodePtr=nodePtr->next;
}
small_end_ptr->next=big_head.next;
big_end_ptr->next=nullptr;
return small_head.next;
}
};
2.7合并两个有序链表
Leetcode-24 合并两个有序链表
思路:创建一个空的头结点,分别遍历两个链表并以此加入到新的链表中,由于需要从小到大的排序,因此采用尾插法的 插入方法,需要新的指针记录尾部结点的位置,最后还需要将尾部结点的next指针域指向NULL;
巧用头空指针
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode new_head(0);
ListNode *lastPtr=&new_head;
while(l1 && l2)
{
if(l1->val <l2->val)
{
//l1加入
lastPtr->next=l1;
lastPtr=l1;
l1=l1->next;
}
else
{
lastPtr->next=l2;
lastPtr=l2;
l2=l2->next;
}
}
if(l1)
{
lastPtr->next=l1;
}
else
{
lastPtr->next=l2;
}
return new_head.next;
}
};
2.8 合并K个升序链表
Leetcode-23合并K个升序链表
思路:将lists中链表合并的问题,划分为前一半和后一半合并的问题,在接着划分子问题,知道划分到链表两两合并的问题,分而治之。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//链表两两合并的代码
ListNode new_head(0);
ListNode *lastPtr=&new_head;
while(l1 && l2)
{
if(l1->val <l2->val)
{
//l1加入
lastPtr->next=l1;
lastPtr=l1;
l1=l1->next;
}
else
{
lastPtr->next=l2;
lastPtr=l2;
l2=l2->next;
}
}
if(l1)
{
lastPtr->next=l1;
}
else
{
lastPtr->next=l2;
}
return new_head.next;
}
ListNode* mergeKLists(vector<ListNode*>& lists) {
if(lists.size()==0)
return nullptr;
if(lists.size()==1)
return lists[0];
if(lists.size()==2)
return mergeTwoLists(lists[0],lists[1]);
int mid=lists.size()/2;
vector<ListNode *> sub1_lists;
vector<ListNode *> sub2_lists;
for(int i=0;i<mid;i++)
{
sub1_lists.push_back(lists[i]);
}
for(int i=mid;i<lists.size();i++)
{
sub2_lists.push_back(lists[i]);
}
ListNode *l1=mergeKLists(sub1_lists);//前半部分:分治1
ListNode *l2=mergeKLists(sub2_lists);//后半部分:分治2
return mergeTwoLists(l1,l2);
}
};
加油