最简单的链表结构是单链表,单链表的长度可以扩充,一个链表包含零个或者多个节点,链表中首元结点通过头指针first找到,其他节点地址在前驱结点的link中
注意循环链表不是线性表
废话不多说,先看看最常见的插入删除操作
1.单链表的插入
当新数据要插在第一个节点之前,那么头指针指向的地址变为newNode指针指向的地址,
newNode->link = first; first = newNode;
如果不在第一个节点之前插入,当前节点指针赋给新数据指针,当前节点指针指向新数据,这样就插进来新数据了
newNode->link=currNode->link; currNode->link = newNode;
bool List::Insert(int i,int& x){
//将新元素插入到 i 节点之后,i 从1开始,i=0表示在第一个节点之前插入
if(first == null || i=0){ //插入空表或者是第一个节点之前
LinkNode *newNode = new LinkNode(x); //创建新节点
if(newNode == null) break;
newNode->link = first;
first = newNode;
}else{
LinkNode *currNode = first;
for(int k=1;k<i;k++){ //从第一个节点开始检测,找到 i 节点
if(currNode==null) break;
else currNode = currNode->link; //当k-1时,最后一轮循环,currNode是他指针指向的位置及 i 节点
}
if(currNode == null) return 0;
else{
LinkNode *newNode = new LinkNode(x);
if(newNode == null) break;
newNode->link = currNode->link;
currNode->link = newNode;
}
}
return true;
}
2.单链表的删除
在首元结点处删除,需要一个指针保存首元结点的地址,将头指针指向下一节点,并删除指针
del=first; first=first->link; delete del;
如果在中间或者后面删除,先用del指针保存 i 节点的地址,即i-1节点的指针指向,让i-1节点的link指针指向i+1节点,在删除del指针
del=currNode->link; currNode->link = del->link; delete del;
bool List::Remove(int i,int& x){
LinkNode *del,*currNode;
if(i<=1){
del = first;
first = first->link;
}else{
currNode = first;
for(int k=1;k<i-1;k++){ //之所以小于i-1是因为currNode指的是i-1节点
if(currNode == null) break;
else currNode = currNode->link;
}
if(currNode == null || currNode->link == null){ //空表或者链太短
return false;
}else{
del = currNode->link;
currNode->link = del->link;
}
}
x - del->data;
delete del; //删除
return true;
}
带附加头结点的单链表
这样就是指first->link = a1;first的link指针是第一个元素
在附加头结点的单链表中插入时不区分位置,直接可以归并为一种情况处理
LinkNode *p; p !=null;这一句不可少,确保是在链表中
建立单链表
建立单链表有两种方式,前插和后插
1.前插法建立单链表
前插法指的是每添加一个新的元素都加在第一个结点的前面,变成第一个结点,这样的话结点的逻辑顺序和插入的顺序正好是相反的
void inputFront(T endTag){
//endTag是输入序列结束的标志
LinkNode<T> *newNode;
T val;
makeEmpty(); //链表置为空表
cin>>val;
while(val != endTag){
newNode = new LinkNode<T>(val);
if(newNode == null) break;
newNode->link = first->link;
first->link = newNode;
cin>>val;
}
}
2.后插法建立单链表
后插法指的是每添加一个新的元素都加在链表的尾端,这样的话结点的逻辑顺序和插入的顺序正好是相同的,插入时需要一个尾指针last指向最后一个结点
void inputRear(T endTag){
//endTag是输入序列结束的标志
LinkNode<T> *newNode,*last;
T val;
makeEmpty(); //链表置为空表
cin>>val;
last = first; //最开始尾指针也是头指针
while(val != endTag){
newNode = new LinkNode<T>(val);
if(newNode == null) break;
last->link = newNode; // 插入到尾端
last = newNode;
cin>>val;
}
last->link = null;
}
下面上几道单链表的应用题
1.逆序合并两个带头结点的单链表,如把非递减的两个单链表合并为一个非递增的
检测两个表的项的大小,把小的项插入单链表的前端,有一个检测完之后,把另一个剩下的前插链表前端
void ReverseMerge(linkList& ha,LinkList& hb){
LinkNode *pa,*pb,*last,*q; //q是插入指针
pa = ha->link; //检测指针
pb = hb->link;
ha->link = null; //结果链表指针初始化,用于存放
delete hb;
while(pa != null && pb != null){
if(pa->data <= pb->data)
{q=pa;pa = pa->link;}
else {q=pb;pb = pb->link;}
q->link = ha->link; //前插
ha->link = q;
}
if(pb != null) pa = pb; //处理剩余节点
while(pa != null){
q=pa;pa=pa->link;q->link=ha->link;ha->link=q; //剩下的插入
}
}
这个涉及到的指针较多,先是将ha hb的指针赋给pa和pb,接下来在初始化ha和hb,ha变为空指针然后指向结果,hb没用就丢掉了,接下来比较大小,选择小的项用q来指向,q是用来执行插入操作的指针,将q插入到空的ha中,pa和pb进行循环遍历,当遍历完一个之后,如果pb还有剩余就将其赋给pa,让pa执行在ha中的插入操作,这样就全部逆序插入进去了
2.逆转 ,即链表中结点链接方向逆转,前驱变后继,后继变前驱
逆转有两种方式,一种是不带头结点的逆转,一种是带头结点的逆转
2.1不带头结点的逆转
不带头结点的逆转需要三个指针pre,h和p,h是操作结点,p是后结点,想要逆转h和p的链接方向,还要更前的结点pr,
void Reverse(LinkList& h){
if(h == null)return;
LinkNode *p=h->link,*pr=null;
while(p != null){
h->link = pr; //逆转h指针
pr=h;h=p;p=p->link; //指针前移
}
pr = h->link;
}
2.2带头结点的逆转
带头结点的逆转比较容易理解,就是先将头结点置为空,从原本的链表中取出一个元素,再将其前插,这样就实现了逆转,和前面实现逆序差不多原理
void Reverse(LinkList& L){
LinkNode *p = L->link,*pr;
L->link = null; //置为空
while(p != null){
pr=p;p=p->link; //取元素
pr->link=L->link;L->link=pr //前插
}
}
再来看线性列表的其他变形
1.循环列表
与单链表不同的是在表尾结点的link域中不是指向null而是指向头结点,这样就形成一个圈了,这样的话知道其中一个结点就可以遍历表中任一结点,循环列表中有头指针first和尾指针last
在循环列表最前端插入时,要改变最后一个结点的link指向,如果有尾指针rear就方便很多,不然就得遍历搜索最后一个结点
newNode->link=rear->link;rear->link=newNodel这样就插入了
循环列表的应用:求解约瑟夫问题
约瑟夫问题是n个人围成圆圈,从第一个人开始报数,报到m的人淘汰,直到只剩下最后一个人,胜利
在这个过程中一共执行了n-1趟循环,每次循环连续计数m个
void Josephus(CircList<T>& js,int n,int m){
<span style="white-space:pre"> </span>CircLinkNode<T> *p=js.Locate(1),*pre=null;
<span style="white-space:pre"> </span>int i,j;
<span style="white-space:pre"> </span>for(i=0;i<n-1;i++){<span style="white-space:pre"> </span>//执行n-1次
<span style="white-space:pre"> </span>for(j=1;j<m;j++){<span style="white-space:pre"> </span>//数m-1个人
<span style="white-space:pre"> </span>pre=p;
<span style="white-space:pre"> </span>p=p->link;<span style="white-space:pre"> </span>//最后一轮循环遍历中,p->link赋给p,即p指向的是第m个人
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>cout<<"出列的是"<<p->data<<endl;
<span style="white-space:pre"> </span>pre->link=p->link;delete p;<span style="white-space:pre"> </span>//删除第m个
<span style="white-space:pre"> </span>p=pre->link;
<span style="white-space:pre"> </span>}
}
2.双向列表
双向列表顾名思义就是有两个指针,lLink是指向它前驱结点,rLink指向的是后继结点,链表首元结点的左链指针和尾结点的右链指针指向的都是附加头结点
2.1插入
双向列表的插入有前驱插入和后继插入,可以设置d的值来区分,当d等于0时前驱方向插入,d=1时,后继插入
</pre><pre name="code" class="plain">bool Insert(int i,const T& x,int d){
DblNode<T> *currNode = Locate(i,d);
if(currNode==null) renturn 0;
DblNode<T> *newNode = new DblNode<T>(x);
if(x==null) break;
if(d==0){ //前驱插入
newNode->lLink=currNode->lLink;
currNode->lLink=newNode;
newNode->lLink->rLink=newNode;
newNode->rLink=currNode;
}else{
newNode->rLink=currNode->rLink;
currNode->rLink = newNode;
newNode->rLink->lLink = newNode;
newNode->lLink = currNode;
}
return 1;
}
2.2删除
找到第 i 个结点,从链中摘下并删除
bool Remove(int i,T& x,int d){
DblNode<T> *currNode = Locate(i,d);
if(currNode==null) renturn 0;
currNode->rLink->lLink = currNode->lLink; //从lLink摘下
currNode->lLink->rLink = currNode->rLink; //rLink摘下
x = currNode->data;
delete currNode;
return 1;
}
各种链表的应用
1.以O(1)的时间代价把两个链表联系起来
这种情况下采用的是单循环列表的结构,要想时间代价是O(1),只能是采用8字型的,将两个链表链接起来,首尾相连的方式要遍历找到最后一个结点,时间代价是O(n)
LinkNode *p=L2->link; L2->link=L1->link; L1->link=p
L1和L2指针的方向交换,先L1连接起来L2,L2在链接起来L1
2.以O(1)为代价,删除链表指针p指示的结点
如果采用的是单循环链表
q=p->link; p->data=q->data; p->link=q->link;delete q;
采用双向链表
p->lLink->rLink=p->rLink;p->rLink->lLink=p->lLink;delete p;