入门链表的世界
很久之前做题目的时候就遇到了链表的概念,但是一直都是断断续续的用一点学一点。这次在做OJ题目时,发现了很多需要用到链表的题目,例如最直接相关的就是多项式加法和乘法链表的反转,打印单链表……这些题目用到链表结构的时候都是很简单直接的,不用的话就会想很久,coding的时候都没有结构性,容易出错。而且在面试的时候,经常要手写链表的代码。索性今天就彻底理解一下单链表,以后遇到了题目就不怕了。
1. 链表的概念
链表是最基本的数据结构,其主要靠指针。首先声明一个指针的结点:
struct node
{
int data;
node *pNext;
};
形象上来说,链表就是一个节点连接一个节点,每个节点的结构几乎都是一样的(data和指向下一个节点的指针)。这种结构很像我们小时候玩的玩具蛇:
我们要取当前节点的下一个节点就直接用p->pNext即可,而要取前面的节点,就要从head开始遍历一边。而在对链表的操作中,很多时候用到了指针,其中一些例如“两个前后的指针,以及一个指针每次移动两步,一个指针每次移动一步”的算法思想,一定要深刻掌握。
2. 相关程序
在面试中,链表的问题经常被问到,而且经常要求现场手写代码。下面列出一些单链表的题目,如下。这些题目主要包括:
- 创建一个链表;
- 增加链表的节点;
- 删除链表的节点;
- 单链表节点的个数;
- 单链表反转;
- 单链表的倒数第K个节点;
- 查找单链表的中间节点;
- 从尾到头打印单链表;
- 已知两个单链表已经有序,把它们合并之后依然有序;
- 判断单链表是否有环;
- 判断单链表是否相交;
- 求两个相交单链表的第一个节点;
- 已知一个单链表中存在环,求进入环中的第一个节点;
- 用O(1)的时间复杂度删除一个节点。
#include<iostream>
#include<stack>
using namespace std;
struct node //C++中定义struct的形式和C中不一样
{
int data;
node *pNext;
};
node *head=NULL;
bool creatList() //创建链表,头节点data=0, pNext=NULL
{
head=new node;
if(head==NULL)
return false;
else
{
head->data=0;
head->pNext=NULL;
return true;
}
}
bool addNode(node *addNode) //增加节点
{
if(head==NULL)
return false;
node *p=head->pNext;
node *q=head;
while(p!=NULL) //往后查找到待增加的节点的位置,p在前,q在后
{
q=p;
p=p->pNext;
}
q->pNext=addNode; //插入在找到的位置,由于p在前,故p即为待插入的位置:q->pNext
addNode->pNext=NULL; //插入后,下一个节点给NULL
return true;
}
bool deleteNode(int index) //删除节点
{
if(head==NULL)
return false;
node *p=head->pNext;
int length=0;
while(p!=NULL)
{
length++; //当前链表的长度
p=p->pNext;
}
if(length<index)
return false;
else
{
node *q=head;
p=head;
for(int i=0; i<index; i++) //用p和q来找到需要删除的节点位置
{
q=p; //和上面一样,p在前,q在后
p=p->pNext;
}
node *t=p->pNext; //找到p为删除位置,就要把p->pNext和q->pNext连接,释放p节点
q->pNext=t;
free(p);
return true;
}
}
//求单链表的节点个数,主要要首先检查链表是否为空,时间复杂度为O(n)
int getListLength(node *head)
{
if(head==NULL)
return 0;
int length=0;
node *p=head;
while(p!=NULL) //思想很简单,一直往后查询,最后一个节点一定是NULL
{
length++;
p=p->pNext;
}
return length;
}
//单链表的反转, 遍历每一个节点,将其放在新链表头端
node *reverseList(node *head)
{
if(head==NULL || head->pNext==NULL) //如果该单链表为空或只有一个节点,不需要反转,就是本身
return head;
node *reverseHead=NULL;
node *p=head;
while(p!=NULL)
{
node *q=p;
p=p->pNext;
q->pNext=reverseHead; //这句是核心,本来reverseHead是q的前面节点,现在赋给q->pNext,就是反转了
reverseHead=q; //当前节点反之后再给reverseHead,便于下一次反转
}
return reverseHead;
}
//查找单链表中的倒数第K个节点。
/*
主要思路就是使用两个指针,先让前面的指针走到正向第k个结点,
这样前后两个指针的距离差是k-1,之后前后两个指针一起向前走,
前面的指针走到最后一个结点时,后面指针所指结点就是倒数第k个结点。
这种思路不需要实现求的单链表中节点的个数
*/
node *getKthNode(node *head, int k)
{
if(k==0 || head==NULL)
return NULL;
node *p=head;
node *q=head;
while(k>1 && p->pNext!=NULL) //首先,让p领先q k个节点。这种思想很重要
{
p=p->pNext;
k--;
}
if(k>1 || p==NULL)
return NULL;
while(p->pNext!=NULL) //当p到达尾部时,q指向的就是倒数第k个节点
{
q=q->pNext;
p=p->pNext;
}
return q;
}
//查找单链表的中间节点,对于链表长度为n,中间节点n/2+1
/*
此题可应用于上一题类似的思想。也是设置两个指针,
只不过这里是,两个指针同时向前走,
前面的指针每次走两步,后面的指针每次走一步,
前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。
注意链表为空,链表结点个数为1和2的情况。时间复杂度O(n)。
*/
node *getMiddleNode(node *head)
{
if(head==NULL || head->pNext==NULL)
return head;
node *p=head;
node *q=head;
while(p->pNext!=NULL)
{
p=p->pNext;
q=q->pNext;
if(p->pNext!=NULL)
p=p->pNext; //p要移动两次,q只移动一次
}
return q;
}
//从尾到头打印单链表
/*
对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。所以,
这一题要么自己使用栈,要么让系统使用栈,也就是递归。
注意链表为空的情况。时间复杂度为O(n)。
*/
void reversePrint(node *head)
{
std::stack<node *>s;
node *p=head;
while(p!=NULL)
{
s.push(p); //进栈
p=p->pNext;
}
while(!s.empty())
{
p=s.top(); //利用栈的特点,后进先出,可以实现先打印尾部节点值
cout<<p->data<<'\t';
s.pop(); //打印过的节点就出栈
}
}
void reversePrint2(node *head)
{
if(head==NULL)
return;
else
{
reversePrint2(head->pNext);
cout<<head->data<<'\t';
}
}
//已知两个单链表head1和head2都是有序的,把它们合并之后,依旧有序
//这个类似归并排序。尤其注意两个链表都为空,和其中一个为空时的情况。只需要O(1)的空间。时间复杂度为O(max(len1, len2))
node *mergeSortedList(node *head1, node *head2)
{
if(head1==NULL)
return head2;
if(head2==NULL)
return head1;
node *mergeHead=NULL;
if(head1->data<head2->data) //如果当前head1较小,则先让mergeHead指向head1
{
mergeHead=head1;
mergeHead->pNext=NULL;
head1=head1->pNext;
}
else
{
mergeHead=head2;
mergeHead->pNext=NULL;
head2=head2->pNext;
}
node *q=mergeHead;
while(head1!=NULL && head2!=NULL)
{
if(head1->data<head2->data)
{
q->pNext=head1;
head1=head1->pNext;
q=q->pNext;
q->pNext=NULL;
}
else
{
q->pNext=head1;
head1=head1->pNext;
q=q->pNext;
q->pNext=NULL;
}
}
if(head1!=NULL)
q->pNext=head1;
else if(head2!=NULL)
q->pNext=head2;
return mergeHead;
}
//上面的解法思路不是很好,其实较为简洁的还是用递归
node *mergeSortedList2(node *head1, node *head2)
{
if(head1==NULL)
return head2;
if(head2=NULL)
return head1;
node *mergeHead=NULL;
if(head1->data<head2->data)
{
mergeHead=head1;
mergeHead->pNext=mergeSortedList(head1->pNext, head2); //递归
}
else
{
mergeHead=head2;
mergeHead->pNext=mergeSortedList(head1, head2->pNext);
}
return mergeHead;
}
//判断一个单链表中是否有环
/*
这里也是用到两个指针。如果一个链表中有环,也就是说用一个指针去遍历,
是永远走不到头的。因此,我们可以用两个指针去遍历,
一个指针一次走两步,一个指针一次走一步,
如果有环,两个指针肯定会在环中相遇。时间复杂度为O(n)。
*/
bool hasCircle(node *head)
{
node *pFast=head;
node *pSlow=head;
while(pFast!=NULL && pFast->pNext!=NULL)
{
pFast=pFast->pNext->pNext; //pFast每次移动两步
pSlow=pSlow->pNext; //pSlow每次移动一步
if(pSlow==pFast)
return true;
}
return false;
}
//判断两个单链表是否相交
/*
如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。
也就是说,如果两个链表相交,那么最后一个节点肯定是共有的。
先遍历第一个链表,记住最后一个节点,然后遍历第二个链表,
到最后一个节点时和第一个链表的最后一个节点做比较,
如果相同,则相交,否则不相交。时间复杂度为O(len1+len2),
因为只需要一个额外指针保存最后一个节点地址,空间复杂度为O(1)。
*/
bool isIntersected(node *head1, node *head2)
{
if(head1==NULL || head2==NULL)
return false;
node *p=head1;
while(p->pNext!=NULL) //p最后指向链表1的尾部
p=p->pNext;
node *q=head2;
while(q->pNext!=NULL) //q最后指向链表2的尾部
q=q->pNext;
return p==q; //如果p==q,则一定相交
}
//求两个单链表相交的第一个节点
/*
对第一个链表遍历,计算长度len1,同时保存最后一个节点的地址。
对第二个链表遍历,计算长度len2,同时检查最后一个节点是否和第一个链表的最后一个节点相同,若不相同,不相交,结束。
两个链表均从头节点开始,假设len1大于len2,
那么将第一个链表先遍历len1-len2个节点,
此时两个链表当前节点到第一个相交节点的距离就相等了,
然后一起向后遍历,直到两个节点的地址相同。
时间复杂度,O(len1+len2)。
*/
node *getFirstCommonNode(node *head1, node *head2)
{
if(head1==NULL || head2==NULL)
return NULL;
int len1=1;
node *p=head1;
while(p->pNext!=NULL) //计算链表1的长度
{
p=p->pNext;
len1++;
}
int len2=1;
node *q=head2;
while(q->pNext!=NULL) //计算链表2的长度
{
q=q->pNext;
len2++;
}
if(p!=q) //如果尾节点都不相等,则就不会相交了
return NULL;
// 先对齐两个链表的当前结点,使之到尾节点的距离相等
p=head1; q=head2;
if(len1>len2)
{
int k=len1-len2;
while(k--)
p=p->pNext;
}
else
{
int k=len2-len1;
while(k--)
q=q->pNext;
}
while(p!=q)
{
p=p->pNext;
q=q->pNext;
}
return p; //返回相等节点
}
//已知一个单链表中存在环,求进入环中的第一个节点
/*
首先判断是否存在环,若不存在结束。
在环中的一个节点处断开(当然函数结束时不能破坏原链表),
这样就形成了两个相交的单链表,
求进入环中的第一个节点也就转换成了求两个单链表相交的第一个节点。
*/
node *getFirstNodeInCircle(node *head)
{
if(head==NULL || head->pNext==NULL)
return NULL;
//先判断是否有环
node *pFast=head;
node *pSlow=head;
while(pFast!=NULL && pFast->pNext!=NULL)
{
pFast=pFast->pNext->pNext; //pFast每次移动两步
pSlow=pSlow->pNext; //pSlow每次移动一步
if(pSlow==pFast)
break;
}
if(pFast==NULL || pSlow==NULL) //说明无环
return NULL;
node *pAssume=pSlow;
node *head1=head; //两个假设的单链表的头节点
node *head2=pAssume->pNext;
//判断两个相交链表的第一个相交点
int len1=1;
node *p=head1;
while(p->pNext!=NULL) //计算链表1的长度
{
p=p->pNext;
len1++;
}
int len2=1;
node *q=head2;
while(q->pNext!=NULL) //计算链表2的长度
{
q=q->pNext;
len2++;
}
if(p!=q) //如果尾节点都不相等,则就不会相交了
return NULL;
// 先对齐两个链表的当前结点,使之到尾节点的距离相等
p=head1; q=head2;
if(len1>len2)
{
int k=len1-len2;
while(k--)
p=p->pNext;
}
else
{
int k=len2-len1;
while(k--)
q=q->pNext;
}
while(p!=q)
{
p=p->pNext;
q=q->pNext;
}
return p; //返回相等节点
}
//用O(1)的时间复杂度删除一个节点
/*
对于删除节点,我们普通的思路就是让该节点的前一个节点指向该节点的下一个节点,
这种情况需要遍历找到该节点的前一个节点,时间复杂度为O(n)。
对于链表,链表中的每个节点结构都是一样的,
所以我们可以把该节点的下一个节点的数据复制到该节点,然后删除下一个节点即可。
要注意最后一个节点的情况,这个时候只能用常见的方法来操作,先找到前一个节点,
但总体的平均时间复杂度还是O(1)。
*/
void Delete(node *head, node *deletedNode)
{
if(deletedNode==NULL)
return;
if(deletedNode->pNext!=NULL) //如果不是链表中的最后一个节点
{
deletedNode->data=deletedNode->pNext->data; //将删除节点的下一个节点的值赋值给删除节点,然后把下一个节点删除
node *p=deletedNode->pNext;
deletedNode->pNext=deletedNode->pNext->pNext;
delete p; //删除下一个节点
}
else //如果是链表的最后一个节点
{
if(head==deletedNode) //如果头节点就是要删除的节点,令head=NULL即可
{
head=NULL;
delete deletedNode;
}
else
{
node *p=head;
while(p->pNext!=deletedNode) //找到倒数第二个节点
p=p->pNext;
p->pNext=NULL;
delete deletedNode;
}
}
}
int main()
{
return 0;
}