2.3 线性表的链式表示
2.3.1 单链表的定义
-
单链表:线性表的链式存储称为单链表,它通过一组任意的存储单元来存储线性表中的数据元素,每个链表结点除了存放自身的信息,还需存放一个指向其后继的指针,来建立数据元素之间的线性关系
-
单链表结点类型描述
typedef struct LNode{ ElemType data; struct LNode *next; }LNode, *LinkList; /* 上述代码等价于:typedef struct LNode LNode; * typedef struct LNode *LinkList * 强调这是一个单链表:使用LinkList * 强调这是一个结点:使用LNode* */
-
头指针:用于标识一个链表,如单链表L,L指向链表中的第一个结点(L本身指向第一个结点,无next指针)
-
头结点:在单链表第一个存储数据元素的结点之前附加的一个结点,头结点的数据域可以不设任何信息,也可以存储链表的长度,头结点的指针域指向线性表的第一个元素结点(即第一个存储数据元素等等结点)
- 优点:
- 由于第一个数据结点的位置存放在头结点的指针域中,因此链表的第一个位置上的操作和在表上其它位置上的操作一致,无需特殊处理
- 无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理得到了统一
-
优点:不需要连续的存储空间,充分利用存储空间,插入和删除速度快
-
缺点:无法逆向检索,查找一个结点后继时间复杂度O(1),查找前驱结点是O(n)
-
基本操作—初始化单链表
bool InitList(LinkList &L){ L=(LNode*)malloc(sizeof(LNode)); if(L==NULL) return false; //内存不足,分配失败 L->next=NULL; // 头结点的指针域为空 return true; }
2.3.2 单链表上基本操作的实现
-
头插法建立单链表
// 有头结点 LinkList List_HeadInsert(LinkList &L){ LNode *s, int x; InitList(L); scanf("%d", &x); while(x!=9999){ s=(LNode)malloc(sizeof(LNode)); s->data=x; s->next=L->next; L->next=s; scanf("%d", &x); } return L; } // 无头结点 LinkList List_HeadInsert(LinkList &L){ LNode *s, int x; InitList(L); scanf("%d", &x); while(x!=9999){ s=(LNode)malloc(sizeof(LNode)); s->data=x; s->next=L; L=s; scanf("%d", &x); } return L; }
- 每个结点插入时间为O(1)
- 建立单链表:O(n)
-
尾插法建立单链表
LinkList List_TailInsert(LinkList &L){ InitList(L); int x; LNode *s, *r; r=L; scanf("%d", &x); while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; /* * 无头结点 * if(r==NULL){ * s->next=NULL; * r=s; * } */ r->next=s; r=s; scanf("%d",&x); } r->next=null; return L; }
-
按序号查找
/* * 1. 根据给定序号i查找第i个元素 * 2. 若i=0则返回头结点,若i<1则返回NULL * 3. 遍历单链表L找到第i个结点 */ LNode *GetElem(LinkList L, int i){ if(i==0){ return L; } if(i<1) return NULL; /* * 无头结点 * if(i<1) return NULL; * if(i==1) return L; * * LNode* p=L; * */ LNode *p=L->next; int count=1; while(p && count<i){ p=p->next; count++; } return p; }
- 时间复杂度为O(n)
-
按值查找结点
/* * 1. 根据给定的值,在单链表L中查找 * 2. 从第一个数据结点开始查找,若指针不为空且值不匹配,则继续比较下一个结点 */ LNode *LocateElem(LinkList L, ElemType e){ LNode *p=L->next; while(p && p->data!=e){ p=p->next; } return p; }
- 时间复杂度:O(n)
-
插入结点
/* * 1. 在第i个位置插入一个新的结点,记为p * 2. 检查i的合法性,i>=1 * 3. 使用GetElem()找到第i个结点的前驱结点第i-1个结点,记为q */ // 前插操作:在某结点前面插入一个新结点 bool ListInsert(LinkList &L, int i, LNode *p){ if(i<1) return false; if(!p) return false; LNode *q=GetElem(L, i-1); p->next=q->next; q->next=p; return false; } // 后插操作:在某节点后面插入一个新的结点 bool LsitInsert(LinkList &L, int i, LNode *p){ if(i<1) return false; if(!p) return false; LNode *q=GetElem(L, i); p->next=q->next; p->next=q; ElemType temp=q->data; q->data=p->data; p->data=temp; return true; }
- 时间复杂度为O(1)
-
删除结点
bool ListDelete(LinkList L, int i){ if(i<1) return false; if(!p) return false; LNode* q=GetElem(L, i-1); LNode* p=q->next; q->next=p->next; free(q); return false; }
-
求链表长度(链表长度不包括头结点)
/* * 遍历链表即可 */ // 有头结点 int Length(LinkList L){ int count=0; LNode *p=L->next; while(p){ count++; p=p->next; } return count; } // 无头结点 int Length(LinkList L){ int count=0; LNode *p=L; while(p){ count++; p=p->next; } return count; }
2.3.3 双链表
-
双链表:双链表结点中有两个指针prior和next,分别指向其前驱结点和后继结点
-
双链表的结点类型描述
typedef struct DNode{ ElemType data; struct DNode *prior, *next; }DNode, *DLinkList;
-
初始化
bool InitDLinkList(DLinkList &L){ DNode L=(DNode*)malloc(sizeof(DNode)); if(L==NULL) return false; s->next=NULL; s->prior=NULL; return true; }
-
插入操作
bool DListInsert(DLinkList &L, int i, ElemType *e){ if(i<1) return false; if(!e) return false; DNode *p=GetElem(L, i-1); e->next=p->next; e->prior=p; if(p->next){ //需保证后继结点存在 p->next->prior=e; } p->next=e; return false; }
-
删除操作
bool DlinkListDelete(DLinkList &L, int i){ if(i<1) return false; DNode *p=GetElem(L, i-1); DNode *q=p->next; p->next=q->next; if(q->next){ q->next->prior=p; } free(q); return true; }
2.3.4 循环单链表
-
循环单链表:循环单链表和单链表的区别是表中最后一个结点的后继指针指向头结点,而不是指向NULL
-
优点:可以在任意位置开始往后遍历整个链表
-
当对单链表的操作需要在表头和表尾进行时,对循环单链表不设头指针而仅设尾指针,使操作效率更高,因为设头指针的话,对表尾进行操作需要O(n)时间复杂度,而从尾指针r到头指针只需要r->next,时间复杂度为O(1)
-
初始化
bool InitList(DLinkList &L){ L=(DNode*)malloc(sizeof(DNode)); if(L==NULL) return false; L->prior=L; L->next=L; return true; }
-
判断空表
bool Empty(DLinkList L){ if(!L) return true; if(L->next=L) return true; else return false; }
2.3.5 循环双链表
-
循环双链表:循环双链表和双链表的区别是,头结点的prior指针指向表尾结点,表尾结点的next指针指向头结点
-
插入操作
bool DListInsert(DLinkList &L, int i, ElemType *e){ if(i<1) return false; if(!e) return false; DNode *p=GetElem(L, i-1); e->next=p->next; e->prior=p; p->next->prior=e; //不需要判断p是否有后继结点,删除操作也一样 p->next=e; return false; }
2.3.6 静态链表
-
静态链表:借助数组来描述线性表的链式存储结构,即分配一整片连续的存储空间来存储数据元素,每个结点也有数据域和指针域,但是指针域(也称游标)存储的是下一个结点的所在的数组的下标
-
静态链表的结构类型描述
#define MaxSize 100 typedef struct{ ElemType data; int next; }SLinkList[MaxSize];
-
下标为0的位置存放1头结点,尾结点的next值为-1
-
静态链表的插入删除操作与动态链表相同,只需要修改指针,而不需要移动元素
-
静态链表没有动态链表方便,但是适用于不支持指针的高级语言