链表基础
1.链表操作
删除、插入、查找、排序、合并、复制
单链表的操作
GelElem(Lnode *head,int i): 找到第i个元素。(必须从头指针出发寻找,复杂度为O(n))
Insert(Lnode *head,int i,ElemType e): 在位置i之前插入元素e。(对于线性表的非线性存储结构,在已知链表中元素的插入或删除的确切位置的情况下,在单链表中插入或者删除一个节点时,只需要修改指针而不需要移动元素)(必须找到第i-1个节点,复杂度为O(n))
Delete(Lnode *head,int i):删除第i个节点。(必须找到第i-1个节点,复杂度为O(n))
remove(Lnode *head,Lnode *ToBeRemoved): 删除某一节点。(对于非尾结点,复杂度为O(1),尾结点的复杂度为O(n))
length(struct Lnode*head):求长度。(从头遍历,复杂度为O(n))
MergeList(Lnode *a,Lnode *b,Lnode *c):将2个有序链表合并为一个有序链表。(时间复杂度?空间复杂度?)
删除操作的说明:
(1)单向链表。
对于带头结点的单向列表,分两种情况讨论,如果待删除的不是尾结点,则直接将后面的结点往前复制;如果待删除的是尾结点,则需要从头开始找到尾结点前面的一个结点,将尾结点的next指针置空。可以在O(1)复杂度下删除某一非尾部结点,删除尾部结点时复杂度为O(n)。
循环单向链表的操作与线性单向链表基本一致,差别仅仅在于算法中的循环条件不是p或者p->next是否为空,而是它们是否为头指针。
但是在循环列表中仅设尾指针而不设头指针时,可以将某些操作简单化,比如将2个线性表合并时,仅需将一个表的表尾和另一个表的表头相连接,运算时间为O(1)。
2.存储结构
线性存储;(数组存储,插入或者删除时需要移动数组)
静态链表;(也用数组表示,其中一个分量表示结点,另一个分量代替指针来表示结点在数组中的相对位置。插入或者删除时不需要移动元素,仍具有链式结构存储的主要优点)
单链表:不带表头结点的单链表,head指向首结点,当head==NUll,为空表,否则为非空表;
带表头结点的单链表,head指向头结点,data为null,pnext指向首结点,当head->pNext==NULL时为空表。
#include"stdlib.h" //或"malloc.h",动态存储分配头文件 #include"stdio.h" //scanf需要的头文件 #include <iostream> using namespace std; #define NULL 0 //定义空指针NULL struct Lnode //Lnode为结点(结构)类型 { int data; //假定data为整型 struct Lnode *next;//next为指针类型 }; #define LENG sizeof(struct Lnode) //结点所占的字节数 //生成带表头结点的单链表。一定以0作为末尾结点 struct Lnode*createl() { struct Lnode *head,*tail,*p; int e; head=(struct Lnode*)malloc(LENG); //新建这样一个结点时,data随机,next也是一个随机的地址值 tail=head; do { p=(Lnode*)malloc(LENG); scanf("%d",&e); p->data=e; tail->next=p; tail=p; } while (e); //在检查条件是否为真时,首先执行一次代码块。这里e不为0时继续 tail->next=NULL; //使得尾指针为空。当链表为空时,head->next=NULL return head; } //打印带表头的单链表 void printl(Lnode *head) { struct Lnode* ToBePrint; ToBePrint=head->next; while (ToBePrint!=NULL) { cout<<ToBePrint->data; ToBePrint=ToBePrint->next; } } void main() { Lnode* head=createl(); printl(head); }
//删除单向列表的结点 void remove(Lnode *head,Lnode *ToBeRemoved) { if(!head||!ToBeRemoved||head->next==NULL||ToBeRemoved==head) // 空表||要删除的是head结点 return; if(ToBeRemoved->next!=NULL) //要删除的不是尾结点。如果要删除的是结点i { Lnode *p=ToBeRemoved->next; ToBeRemoved->data=p->data; ToBeRemoved->next=p->next; free(p); //和malloc对应 p=NULL; } else //要删除的是尾结点(包括只有一个结点的情况),必须找到尾结点以前的结点 { Lnode *p=head; while(p->next!=ToBeRemoved) { p=p->next; } p->next=NULL; free(ToBeRemoved); ToBeRemoved=NULL; } } //找到带头结点的单向列表的第k个结点 Lnode* find(Lnode* head,int k) { if(k==0||head->next==NULL) return NULL; int count=1; Lnode* r=head->next; while(count<k) { if(r->next==NULL) //已经是尾结点了,但是仍然没有找到第k个结点 return NULL; r=r->next; count++; } return r; } //求带头结点的线性链表的长度,并依次输出结点的值 int length(struct Lnode*head) { int leng=0; Lnode* p; p=head->next; //p指向首结点 while(p!=NULL) //p非空 { //cout<<p->data; leng++; p=p->next; } return leng; }
循环链表:
循环链表在生成时唯一与单链表不同的就是尾指针指向了头结点
(1)带头结点的循环链表:
非空:H->next≠H, H≠NULL
空:H->next==H, H≠NULL
(2)只设尾指针的循环链表:
非空:tail 指向尾结点,tail->data==an
tail->next 指向表头结点
tail->next->next指向首结点
tail->next->next->data==a1
空:tail->next==tail
双向链表:
(1)非空表:L为头指针,L指向表头结点,L->next指向首结点
L->next->data==a1
L->prior指向尾结点
L->prior->data==an
L->next->prior== L->prior->next==L
(2)空表:L->next==L->prior==NULL
双向循环链表:
(1)空表:L->next==L->prior==L
(2)非空表:设p指向a1, 则:
p->next指向a2 , p->next->prior指向a1
有: p==p->next->prior, p==p->prior->next
题目:删除双向链表中的某个结点
p->prior->next = p->next; //结点A的next指向结点C
p->next->prior = p->prior; //结点C的prior指向结点A
free(p)
(1)对于非循环的双向链表:
题目:双向链表中插入结点
① f->prior=p->prior; //结点B的prior指向结点A
② f->next=p; //结点B的next指向结点C
③ p->prior->next=f; //结点A的next指向结点B
④ p->prior=f; //结点C的prior指向结点B
3.思考
(1).线性表的顺序存储结构有什么优点和缺点?
线性表的顺序存储
优点: 具有简单、运算方便等优点,特别是对于小线性表或长度固定的线性表,采用顺序存储结构的优越性更为突出;
缺点:1.顺序存储插入与删除一个元素,必须移动大了的数据元素,以此对大的线性表,特别是在元素的插入和删除很频繁的情况下,采取顺序存储很是不方便,效率低;
2.顺序存储空间容易满,出现上溢,程序访问容易出问题,顺序存储结构下,存储空间不便扩充;
3.顺序存储空间的分配问题,分多了浪费,分少了空间不足上溢。
对于大的线性表,特别是元素变动频繁的大线性表不宜采用顺序存储空间,而采用链式存储结构。
(2).试比较单链表、双链表、循环链表的优、缺点。
单链表:如果访问任意结点每次只能从头到尾顺序向后访问
单循环链表:可以从任何一个结点开始,顺序向后访问到达任意结点
双向链表:可以从任何结点开始任意向前向后双向访问操作
单链表和单循环链表:只能在当前结点后插入和删除
双链表:可以在当前结点前面或者后面插入,可以删除前趋和后继(包括结点自己)
存储:单链表和单循环链表存储密度大于双链表