线性表的定义和特点
-
线性表的定义
线性表是具有相同特性数据元素的有限序列,数据元素在逻辑结构中可以称为结点
-
起始结点a1,终端结点an,n为元素总个数,即表的长度,n = 0时为空表
-
ai结点的前驱为ai-1,后继为ai+1
-
-
线性表的特点
在非空线性表中:
- 有且仅有一个起始结点a1,它没有直接前驱,有且只有一个直接后继a2;
- 有且仅有一个终端结点an,它没有直接后继,有且只有一个直接前驱an-1;
- 其余的内部结点ai,有且只有一个直接前驱ai-1,也有且只有一个直接后继ai+1
-
线性表的类型定义
ADT List{ 数据对象:D = {ai | ai属于Elemset,i = 1, 2,..., n, n>=0} 数据关系:R = {<ai-1, ai> | ai-1, ai属于D,i = 2, 3,..., n, n>=0} 基本操作: InitList(&L)、DestroyList(&L)、ListInsert(&L, i, e)、ListDelete(&L, i, &e)、... }ADT List
-
基本操作
-
void InitList(&L)
操作结果:构造一个空的线性表L
-
int ListEmpty(L)
初始条件:线性表L已经存在
操作结果:若线性表L为空表,返回True,否则返回False
-
int ListLength(L)
初始条件:线性表L已经存在
操作结果:返回线性表L中的元素个数
-
void DestroyList(&L)
初始条件:线性表L已经存在
操作结果:销毁线性表L
-
void ClearList(&L)
初始条件:线性表L已经存在
操作结果:将线性表L重置为空表
-
ListiTraverse(&L, visited())
初始条件:线性表L已经存在
操作结果:依次对线性表中每个元素调用
visited()
-
void GetElem(L, i, &e)
初始条件:线性表L已经存在,1 <= i <=
ListLength(L)
操作结果:用e返回线性表L中第i个数据元素的值
-
int LocateElem(L, e, compare())
初始条件:线性表L已经存在,
compare()
是数据元素判定函数操作结果:返回L中第1个与e满足
compare()
的数据元素的位序,若这样的数据元素不存在则返回值为0 -
void PriorElem(L, cur_e, &pre_e)
初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,否则操作失败,pre_e无意义
-
void NextElem(L, cur_e, &next_e)
初始条件:线性表L已经存在。
操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无意义
-
void Listlnsert(&L, i, e)
初始条件:线性表L已经存在,1 <= i <=
ListLength(L)
+1操作结果:在L的第i个位置之前插入新的数据元素e,L的长度加1
-
void ListDelete(&L, i, &e)
初始条件:线性表已经存在,1 <= i <=
ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1。
-
-
线性表的顺序存储结构
-
线性表的顺序存储结构的定义
- 顺序存储结构/顺序映像:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构
- 线性表的顺序存储结构又称为顺序表
-
顺序表中元素存储位置的计算
假设顺序表中每个元素占l个存储单元,则第i+1个元素和第i个元素的地址满足关系: L O C ( a i + 1 ) = L O C ( a i ) + l LOC(a_{i+1}) = LOC(a_i)+l LOC(ai+1)=LOC(ai)+l
由此可以推知: L O C ( a i + 1 ) = L O C ( a 1 ) + ( i − 1 ) l LOC(a_{i+1}) = LOC(a_1)+(i-1)l LOC(ai+1)=LOC(a1)+(i−1)l(类比等差数列通项公式)
因此只要知道了基地址(a1的地址)和每个元素所占的存储空间,就用这个公式可以推算出每个元素位置,定位元素的算法时间复杂度与顺序表表长n无关
-
顺序表的特点
- 利用数据元素的存储位置表示线性表中相邻数据元素之间的后天示,即线性表的逻辑结构与存储结构一致
- 在访问线性表时,可以快速地计算出任何一个数据元素的存储地址。因此可以粗略地认为,访问每个元素所花时间相等。这种存取元素的方法被称为随机存取法
-
顺序表定义的实现
#define LIST_INIT_SIZE 100 // 顺序表存储空间的初始分配量 typedef struct { Elemtype elem[LIST_INIT_SIZE]; // 或Elemtype* elem(前者是静态分配,后者可以构造动态分配) // L.data=(Elem Type*)malloc(sizeof(ElemType)* SIZE); int length; // 顺序表表长 }sqList;
-
多项式的顺序表实现
#define MAXSIZE 1000 typedef struct { // 定义多项式数据类型 float e; // 系数 int p; // 指数 }Polynomial; typedef struct { Polynomial* elem; int length; }SqList;
-
图书管理系统
#define MAXSIZE 1000 typedef struct { // 定义图书数据类型 char no[20]; // 图书ISBN char name[30]; // 书名 float price; // 价格 }Book; typedef struct { Book* elem; int length; }SqList;
-
-
顺序表操作的实现
#include<stdio.h> #include<stdlib.h> // 定义布尔值 #define TRUE 1 #define FALSE 0 // 定义状态码 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 // 定义顺序表最大长度 #define MAXSIZE 100 typedef int Status; typedef int boolean; typedef int Elemtype; // 定义顺序表类型 typedef struct { Elemtype* elem; // 通过指针动态分配空间的方法来定义数组 int length; }SqList; /** * 函数传值与传指针的作用: * 不需要改变a的本体,只需要利用a的值进行运算,那就传值 * 需要操作a的本体,那就传指针 */ // 初始化顺序表 Status InitList(SqList* L) { L->elem = (Elemtype*)malloc(sizeof(Elemtype)*MAXSIZE); // 申请数组空间 if(!(L->elem)) { return OVERFLOW; } L->length = 0; return OK; } // 求顺序表的长度 int GetLength(SqList L) { return L.length; } // 判断顺序表是否为空 boolean IsEmpty(SqList L) { if(L.length) { return FALSE; } else { return TRUE; } } // 销毁顺序表 void DestroyList(SqList* L) { if(L->elem) { free(L->elem); } L->elem = NULL; L->length = 0; } // 清空顺序表 void ClearList(SqList* L) { L->length = 0; } /** * 取元素和查找元素的算法思路: * 1. 先判断参数合法性 * 2. 取元素直接取即可,查找元素需要遍历数组 * 两者的共同点是都要注意逻辑序号和物理序号之间差了1 */ // 取顺序表中的元素 Status GetElem(SqList L, int i, Elemtype* e) { if(L.length == 0 || i > L.length || i < 1) { // 数组长度为0、越界访问都是非法的 return ERROR; } *e = L.elem[i-1]; // 逻辑上第i个数据映射到数组中第i-1个数据 return OK; } // 查找顺序表中的元素 int LocateElem(SqList L, Elemtype e) { for(int i = 0; i <= L.length-1; i++) { if(L.elem[i] == e) { return i+1; // 如果找到了,返回序号;数组中第i个数据映射到逻辑上第i+1个数据 } } return 0; // 如果没找到,返回0 } /** * 插入元素和删除元素的算法思路: * 1. 先判断参数的合法性 * 2. 插入元素,从插入位置开始到尾部的元素集体向后挪一格;删除元素,删除位置之后的元素集体向前挪一格 * 向后挪就从最后面开始一个一个挪,向前挪就从最前面开始一个一个挪 */ // 向顺序表中插入元素 Status Listlnsert(SqList* L, int i, Elemtype e) { if(L->length == MAXSIZE || i < 1 || i > L->length+1){ // 数组满了、越界访问都是非法的 return ERROR; } for(int j = L->length-1; j >= i-1; j--) { L->elem[j+1] = L->elem[j]; } L->elem[i-1] = e; L->length++; return OK; } // 从顺序表中删除元素 Status ListDelete(SqList* L, int i, Elemtype* e) { if(L->length == 0 || i < 1 || i > L->length) { // 数组长度为0、越界访问都是非法的 return ERROR; } *e = L->elem[i-1]; for(int j = i-1; j+1 <= L->length; j++) { L->elem[j] = L->elem[j+1]; } L->length--; return OK; } // 建立测试顺序表 void CreateTestList(SqList* L) { for(int i = 1; i <= 20; i++, L->length++) { L->elem[i-1] = i; } } // 展示测试顺序表中的所有元素 void ShowList(SqList L) { for (int i = 0; i <= L.length-2; i++) { printf("%d, ", L.elem[i]); } printf("%d\n", L.elem[L.length-1]); printf("length = %d\n", L.length); }
线性表的链式存储结构
-
线性表的链式存储结构的定义
-
链式存储结构/链式映像:结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
-
线性表的链式存储结构又称为链表
顺序表相当于打饭排队,你必须按顺序站那排,逻辑上的顺序和物理位置上的顺序是一样的
链表相当于银行叫号办业务,逻辑上的顺序定好了后,你可以拿着票子到处走动,不需要站那排队
-
-
链表的结构
-
结点:每个结点分为数据域和指针域
- 数据域存储当前结点的数据
- 指针域存储直接后继结点的地址(最后一个结点的指针域指向NULL)
-
头指针
头指针记录第一个结点的地址,这样就可以通过头指针顺藤摸瓜遍历所有结点了,因此可以用头指针的名称来代表链表的名称
-
头结点
有些链表在首元结点(第一个数据元素)之前设置头结点,头结点的数据域可以记录整个链表的长度等信息,同时也方便了链表的一些操作
-
-
链表的种类
- 单链表:即上述的正常情况下的链表
- 双向链表:有两个指针域,一个指针域指向后继,另一个指针域指向前驱
- 循环链表:最后一个结点的指针域指向第一个结点
-
无头链表与带头链表的比较
-
空表如何表示?
- 无头链表:头指针指向NULL
- 带头链表:头结点的指针域指向NULL
-
设置头结点有什么好处?
-
便于首元结点的处理
首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理
-
便于空表和非空表的统一处理
无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了
-
头结点的数据域内装的是什么?
头结点的数据域可以为空,也可存放线伴表长度等附加信息,但此结点不能计入链表长度值
-
-
-
链表的特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
- 访问时只能通过头指针进入链表,并通过每个结点的指针域一依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。这种存取元素的方法被称为顺序存取法
单链表
-
单链表定义的实现(带头结点)
typedef struct Node { Elemtype data; // 数据域 struct Node* next; // 指针域 }Node, *LinkList; // LinkList是Node的指针类型
-
图书管理系统
typedef struct Node { char no[20]; // 图书ISBN char name[30]; // 书名 float price; // 价格 struct Node* next; // 指针域 }Node, *LinkList; // LinkList是Node的指针类型
为了统一操作,一般这样定义:
typedef struct { char no[20]; // 图书ISBN char name[30]; // 书名 float price; // 价格 }Elemtype; typedef struct Node { Elemtype data; // 数据域 struct Node* next; // 指针域 }Node, *LinkList; // LinkList是Node的指针类型
-
-
单链表的操作的实现
#include<stdio.h> #include<stdlib.h> // 定义布尔值 #define TRUE 1 #define FALSE 0 // 定义状态码 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 // 定义顺序表最大长度 #define MAXSIZE 100 typedef int Status; typedef int boolean; typedef int Elemtype; // 定义顺序表类型 typedef struct { Elemtype* elem; // 通过指针动态分配空间的方法来定义数组 int length; }SqList; /** * 函数传值与传指针的作用: * 不需要改变a的本体,只需要利用a的值进行运算,那就传值 * 需要操作a的本体,那就传指针 */ // 初始化顺序表 Status InitList(SqList* L) { L->elem = (Elemtype*)malloc(sizeof(Elemtype)*MAXSIZE); // 申请数组空间 if(!(L->elem)) { return OVERFLOW; } L->length = 0; return OK; } // 求顺序表的长度 int GetLength(SqList L) { return L.length; } // 判断顺序表是否为空 boolean IsEmpty(SqList L) { if(L.length) { return FALSE; } else { return TRUE; } } // 销毁顺序表 void DestroyList(SqList* L) { if(L->elem) { free(L->elem); } L->elem = NULL; L->length = 0; } // 清空顺序表 void ClearList(SqList* L) { L->length = 0; } /** * 取元素和查找元素的算法思路: * 1. 先判断参数合法性 * 2. 取元素直接取即可,查找元素需要遍历数组 * 两者的共同点是都要注意逻辑序号和物理序号之间差了1 */ // 取顺序表中的元素 Status GetElem(SqList L, int i, Elemtype* e) { if(IsEmpty(L) || i > GetLength(L) || i < 1) { // 数组长度为0、越界访问都是非法的 return ERROR; } *e = L.elem[i-1]; // 逻辑上第i个数据映射到数组中第i-1个数据 return OK; } // 查找顺序表中的元素 int LocateElem(SqList L, Elemtype e) { if(IsEmpty(L)) { // 空表不能查找 return 0; } for(int i = 0; i <= L.length-1; i++) { if(L.elem[i] == e) { return i+1; // 如果找到了,返回序号;数组中第i个数据映射到逻辑上第i+1个数据 } } return 0; // 如果没找到,返回0 } /** * 插入元素和删除元素的算法思路: * 1. 先判断参数的合法性 * 2. 插入元素,从插入位置开始到尾部的元素集体向后挪一格;删除元素,删除位置之后的元素集体向前挪一格 * 向后挪就从最后面开始一个一个挪,向前挪就从最前面开始一个一个挪 */ // 向顺序表中插入元素 Status Listlnsert(SqList* L, int i, Elemtype e) { if(GetLength(*L) == MAXSIZE || i < 1 || i > GetLength(*L)+1){ // 数组满了、越界访问都是非法的 return ERROR; } for(int j = L->length-1; j >= i-1; j--) { L->elem[j+1] = L->elem[j]; } L->elem[i-1] = e; L->length++; return OK; } // 从顺序表中删除元素 Status ListDelete(SqList* L, int i, Elemtype* e) { if(IsEmpty(*L) || i < 1 || i > GetLength(*L)) { // 数组长度为0、越界访问都是非法的 return ERROR; } *e = L->elem[i-1]; for(int j = i-1; j+1 <= L->length; j++) { L->elem[j] = L->elem[j+1]; } L->length--; return OK; } // 建立测试顺序表 void CreateTestList(SqList* L) { ClearList(L); for(int i = 1; i <= 20; i++, L->length++) { L->elem[i-1] = i; } } // 展示测试顺序表中的所有元素 void ShowList(SqList L) { if(!L.elem) { // L已销毁的情况 printf("顺序表已销毁!\n"); return; } else if(IsEmpty(L)) { // L为空表的情况 printf("[]\n"); } else { // L为非空表的情况 printf("["); for (int i = 0; i <= L.length-2; i++) { // 最后一位单独输出 printf("%d, ", L.elem[i]); } printf("%d]\n", L.elem[L.length-1]); // 最后一位单独输出 } printf("length = %d\n", GetLength(L)); }
-
测试
#include<stdio.h> #include"SqList.h" int main() { SqList L; Elemtype e; // 测试InitList(&L) printf("InitList(&L)"); getchar(); InitList(&L); ShowList(L); getchar(); // 测试长度为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试CreateTestList(&L) printf("CreateTestList(&L)"); getchar(); CreateTestList(&L); ShowList(L); getchar(); // 测试长度不为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试ClearList(&L) printf("ClearList(&L)"); getchar(); ClearList(&L); ShowList(L); getchar(); // 测试CreateTestList(&L) printf("CreateTestList(&L)"); getchar(); CreateTestList(&L); ShowList(L); getchar(); // 测试LocateElem(L, 10) printf("LocateElem(L, 10)"); getchar(); printf("%d\n", LocateElem(L, 10)); getchar(); // 测试Listlnsert(&L, 5, 111) printf("Listlnsert(&L, 5, 111)"); getchar(); Listlnsert(&L, 5, 111); ShowList(L); getchar(); // 测试ListDelete(&L, 3, &e); printf("ListDelete(&L, 3, &e)"); getchar(); ListDelete(&L, 3, &e); ShowList(L); printf("%d\n", e); getchar(); // 测试DestroyList(&L) printf("DestroyList(&L)"); getchar(); DestroyList(&L); ShowList(L); system("pause"); return 0; }
-
单链表的优缺点
-
操作的算法分析
-
查找
因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为O(n)
-
插入和删除
因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为O(1)
但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为O(n)
-
-
循环链表
-
循环链表操作的实现
#include<stdio.h> #include<stdlib.h> // 定义布尔值 #define TRUE 1 #define FALSE 0 // 定义状态码 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status; typedef int boolean; typedef int Elemtype; // Elemtype类型可以根据需求改动 typedef struct Node { Elemtype data; // 数据域 struct Node *next; // 指针域(带有递归的思想) }Node, *CirLinkList; // CirLinkList是Node的指针类型 // 通过尾指针控制循环链表 // 初始化链表 Status InitList(CirLinkList *R) { *R = (CirLinkList)malloc(sizeof(Node)); // 申请头结点 if(!(*R)) { return OVERFLOW; } (*R)->data = 0; // 头结点数据域初始化为0 (*R)->next = *R; // 头结点指针域指向自身 return OK; } // 求链表的长度 int GetLength(CirLinkList R) { int i; CirLinkList p; // 尾指针需要作为结束标志,因此需要用新指针来移动 if(R == R->next) { return 0; } for(i = 1, p = R->next->next; p != R; i++) { p = p->next; } return i; } // int GetLength(CirLinkList R) { // return R->next->data; // 头结点数据域就存放着链表的长度,可以直接调用 // } // 判断链表是否为空 boolean IsEmpty(CirLinkList R) { if(R != R->next) { // 尾指针指的不是头结点就非空 return FALSE; } else { return TRUE; } } /** *销毁链表和清空链表的算法思路: *“过河拆桥”算法,采用前后指针 *前指针先去下一个结点,后指针释放当前结点,再跟着前指针去下一个结点 *直到前指针指向尾结点,然后后指针跟上也指向尾结点时结束 *清空链表也不能到头结点,因为这里用的是尾指针,释放尾结点会导致指针地址丢失 *区别是销毁链表要连头结点一起销毁掉,清空链表要保留头结点 */ // 销毁链表 void DestroyList(CirLinkList *R) { CirLinkList p, q; // q作为前指针,p指针作为后指针(尾指针需要作为结束标志) p = (*R)->next; // 后指针p初始化指向头结点 while(p != *R) { // 后指针p指向尾结点时结束 q = p->next; // 前指针*L先去下一个结点 free(p); // 后指针p释放当前结点 p = q; // 后指针p跟着前指针*L去下一个结点 } free(p); // 释放尾结点 *R = NULL; } // 清空链表 void ClearList(CirLinkList *R) { if(IsEmpty(*R)) { // 空表不用清空 return; } CirLinkList p, q; // q作为前指针,p指针作为后指针(尾指针需要作为结束标志) p = (*R)->next->next; // 后指针p初始化指向首元结点 while(p != *R) { // 后指针p指向尾结点时结束 q = p->next; // 前指针*L先去下一个结点 free(p); // 后指针p释放当前结点 p = q; // 后指针p跟着前指针*L去下一个结点 } *R = (*R)->next; // 此时只剩尾结点和头结点,将尾指针移到头结点处 free(p); // 释放尾结点 (*R)->data = 0; // 头结点数据域清0 (*R)->next = *R; // 头结点指针域指向自身 } /** *取元素和查找元素的算法思路: *1. 先判断参数合法性 *2. 再依次遍历每个元素,加个if判断即可 */ // 取链表中的元素 Status GetElem(CirLinkList R, int i, Elemtype *e) { if(IsEmpty(R) || i > GetLength(R) || i < 1) { // 链表长度为0、越界访问都是非法的 return ERROR; } int j; CirLinkList p; // 尾指针需要作为结束标志,因此需要用新指针来移动 for(j = 1, p = R->next->next; p != R->next; j++) { if(j == i) { *e = p->data; return OK; } p = p->next; } return ERROR; } // 查找链表中的元素 int LocateElem(CirLinkList R, Elemtype e) { if(IsEmpty(R)) { // 空表直接不能查找 return 0; } int i; CirLinkList p; // 尾指针需要作为结束标志,因此需要用新指针来移动 for(i = 1, p = R->next->next; p != R->next; i++) { if(p->data == e) { return i; } p = p->next; } return 0; // 如果没找到,返回0 } /** *插入元素和删除元素的算法思路: *1. 先判断参数的合法性 *2. 再依次遍历每个元素,找到待操作结点的前驱 * 如果要插入元素,需要拿一个新指针去申请结点,然后再进行逻辑上的插入操作 * 如果要删除元素,也需要哪一个新指针去保存要删除元素的地址,然后再进行逻辑上的删除操作, *以免在进行逻辑上的删除操作后丢失地址 * 找前驱的目的是方便逻辑上的操作,用新指针的目的是方便物理上的操作 */ // 向链表中插入元素 Status Listlnsert(CirLinkList *R, int i, Elemtype e) { if(i < 1 || i > GetLength(*R)+1){ // 越界访问是非法的(+1是指可以插在最尾部) return ERROR; } int j, insertInTail; CirLinkList p, s; insertInTail = 0; if(i == GetLength(*R)+1) { // 如果插在最尾部,后面需要移动尾指针 insertInTail = 1; } for(j = 1, p = (*R)->next->next; p != (*R)->next; j++) { if(j == i-1) { // 找到新结点的前驱 s = (CirLinkList)malloc(sizeof(Node)); // 申请新结点 if(!s){ return OVERFLOW; } s->data = e; // 新结点数据域赋值e s->next = p->next; // 新结点指针域指向其前驱原来的后继 p->next = s; // 新结点前驱指针域指向新结点 if(insertInTail) { // 如果插在最尾部,需要移动尾指针 *R = (*R)->next; } (*R)->next->data++; return OK; } p = p->next; } return ERROR; } // 从链表中删除元素 Status ListDelete(CirLinkList *R, int i, Elemtype *e) { int j, deleteInTail; CirLinkList p, q; if(IsEmpty(*R) || i < 1 || i > GetLength(*R)) {// 链表长度为0、越界访问都是非法的 return ERROR; } deleteInTail = 0; if(i == GetLength(*R)) { // 如果在最尾部删除,后面需要移动尾指针 deleteInTail = 1; } for(j = 1, p = (*R)->next->next; p != (*R)->next; j++) { if(j == i-1) { // 找到待删除结点的前驱 q = p->next; // q指针保存待删除结点地址,以免释放空间时地址丢失 *e = q->data; // e变量保存待删除结点的数据域 p->next = q->next; // 待删除结点前驱指针域指向待删除结点后继 free(q); // 释放待删除结点的空间,完成删除操作 if(deleteInTail) { // 如果在最尾部删除,需要移动尾指针 *R = p; } (*R)->next->data--; return OK; } p = p->next; } return ERROR; } /** *头插法和尾插法建立链表的算法思路: *头插法是不断申请新结点,将新结点插入到头结点和原有首元结点中间 *尾插法是不断申请新结点,将新结点插入到尾部 *两者算法上的最大区别是,头插法需要盯住头结点来进行逻辑操作,尾插法需要盯住尾结点进行操作 *头结点是不会变的,而尾结点是一直在变的,所以尾插法需要在插入后更新尾指针 */ // 头插法建立测试链表 Status CreateTestListByHead(CirLinkList *R) { ClearList(R); int i; CirLinkList p; for(i = 1; i <= 20; i++) { p = (CirLinkList)malloc(sizeof(Node)); // p指针申请新结点 if(!p) { return OVERFLOW; } p->data = i; p->next = (*R)->next->next; // 新结点指向头结点的后继 (*R)->next->next = p; // 头结点指向新节点,完成插入 if(i == 1) { *R = p; } (*R)->next->data++; } return OK; } // 尾插法建立测试链表 Status CreateTestListByTail(CirLinkList *R) { ClearList(R); int i; CirLinkList p; for(i = 1; i <= 20; i++) { p = (CirLinkList)malloc(sizeof(Node)); // p指针申请结点空间 if(!p) { return OVERFLOW; } p->data = i; p->next = (*R)->next; // 新结点指向头结点(尾结点的后继) (*R)->next = p; // 尾结点指向新节点,完成插入 *R = p; // 新结点成为尾结点 (*R)->next->data++; } return OK; } // 合并链表 CirLinkList Connect(CirLinkList Ta, CirLinkList Tb) { if(IsEmpty(Ta) || IsEmpty(Tb)) { // 如果Ta和Tb有一个是空表,就返回那个不是空表的 return Ta?Ta:Tb; // 如果都是空表,显然就返回空表 } CirLinkList p, q; p = Ta->next; // 保存Ta链表头部地址,防止后续操作地址丢失 q = Tb->next; // 保存Tb链表头部地址,防止后续操作地址丢失 Tb->next = Ta->next->next; // Tb链表尾部接到Ta链表头部(首元结点) Ta->next = q; // Ta链表尾部接到Tb链表头部(头结点),完成合并 Ta->next->data += p->data; // 链表长度相加 free(p); // 释放原Ta链表的头结点 return Ta; // Ta指针指向原Ta链表的尾部,此时也是合并链表的尾部 } // 展示测试链表中的所有元素 void ShowList(CirLinkList R) { CirLinkList p; if(!R) { // R已销毁的情况 printf("链表已销毁!\n"); return; } else if(IsEmpty(R)) { // L为空表的情况 printf("[]\n"); } else { // L为非空表的情况 p = R->next->next; printf("["); while(p != R) { // 最后一位单独输出 printf("%d, ", p->data); p = p->next; } printf("%d]\n", p->data); // 最后一位单独输出 } printf("length = %d\n", R->next->data); }
-
测试
#include<stdio.h> #include"CirLinkList.h" int main() { Elemtype e; CirLinkList R, Ta, Tb, T; // 测试InitList(&R) printf("InitList(&R)"); getchar(); InitList(&R); ShowList(R); getchar(); // 测试长度为0时GetLength(R) printf("GetLength(R)"); getchar(); printf("%d\n", GetLength(R)); getchar(); // 测试CreateTestListByHead(&R) printf("CreateTestListByHead(&R)"); getchar(); CreateTestListByHead(&R); ShowList(R); getchar(); // 测试长度不为0时GetLength(L) printf("GetLength(R)"); getchar(); printf("%d\n", GetLength(R)); getchar(); // 测试ClearList(&R) printf("ClearList(&R)"); getchar(); ClearList(&R); ShowList(R); getchar(); // 测试CreateTestListByTail(&R) printf("CreateTestListByTail(&R)"); getchar(); CreateTestListByTail(&R); ShowList(R); getchar(); // 测试LocateElem(R, 10) printf("LocateElem(R, 10)"); getchar(); printf("%d\n", LocateElem(R, 10)); getchar(); // 测试Listlnsert(&R, 5, 111) printf("Listlnsert(&R, 5, 111)"); getchar(); Listlnsert(&R, 5, 111); ShowList(R); getchar(); // 测试Listlnsert(&R, 22, 222) printf("Listlnsert(&R, 22, 222)"); getchar(); Listlnsert(&R, 22, 222); ShowList(R); getchar(); // 测试ListDelete(&R, 3, &e); printf("ListDelete(&R, 3, &e)"); getchar(); ListDelete(&R, 3, &e); ShowList(R); printf("%d\n", e); getchar(); // 测试ListDelete(&R, 21, &e) printf("ListDelete(&R, 21, &e)"); getchar(); ListDelete(&R, 21, &e); ShowList(R); printf("%d\n", e); getchar(); // 测试DestroyList(&R) printf("DestroyList(&R)"); getchar(); DestroyList(&R); ShowList(R); // 测试Connect(LinkList Ta, LinkList Tb) InitList(&Ta); InitList(&Tb); CreateTestListByHead(&Ta); CreateTestListByHead(&Tb); getchar(); printf("Ta: "); getchar(); ShowList(Ta); getchar(); printf("Tb: "); getchar(); ShowList(Tb); getchar(); printf("Connect(LinkList Ta, LinkList Tb)\n"); getchar(); T = Connect(Ta, Tb); ShowList(T); system("pause"); }
-
循环链表的优缺点
优点:从表中任一结点出发均可找到表中其他结点
双向链表
-
双向链表操作的实现
#include<stdio.h> #include<stdlib.h> // 定义布尔值 #define TRUE 1 #define FALSE 0 // 定义状态码 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status; typedef int boolean; typedef int Elemtype; // Elemtype类型可以根据需求改动 typedef struct DuLNode { Elemtype data; // 数据域 struct DuLNode *prior, *next; // 指针域(带有递归的思想) }DuLNode, *DuLinkList; // DuLinkList是DuLNode的指针类型 // 通过头指针控制双向链表 // 初始化链表 Status InitList(DuLinkList* L) { *L = (DuLinkList)malloc(sizeof(DuLNode)); // 申请头结点 if(!(*L)) { return OVERFLOW; } (*L)->data = 0; // 头结点数据域初始化为0 (*L)->prior = NULL; // 头结点prior域指向NULL (*L)->next = NULL; // 头结点next域指向NULL return OK; } // 求链表的长度 // int GetLength(DuLinkList L) { // int i; // for(i = 0; L->next; i++) { // L = L->next; // 传值不改变头指针本体,可以直接拿虚拟的头指针L来移动 // } // return i; // } int GetLength(DuLinkList L) { return L->data; // 头结点数据域就存放着链表的长度,可以直接调用 } // 判断链表是否为空 boolean IsEmpty(DuLinkList L) { if(L->next) { return FALSE; } else { return TRUE; } } /** * 销毁链表和清空链表的算法思路: * “过河拆桥”算法,采用前后指针 * 前指针先去下一个结点,后指针释放当前结点,再跟着前指针去下一个结点 * 直到前指针指向空,然后后指针跟上也指向空时结束 * 区别是销毁链表要连头结点一起销毁掉,清空链表要保留头结点 */ // 销毁链表 void DestroyList(DuLinkList* L) { DuLinkList p; // *L指针(头指针)作为前指针,p指针作为后指针 p = *L; // 后指针p初始化指向头结点 while(p) { // 后指针p指向空时结束 *L = p->next; // 前指针*L先去下一个结点 free(p); // 后指针p释放当前结点 p = *L; // 后指针p跟着前指针*L去下一个结点 } } // 清空链表 void ClearList(DuLinkList* L) { if(IsEmpty(*L)) { // 空表不用清空 return; } DuLinkList p, q; // q指针作为前指针,p指针作为后指针 p = (*L)->next; // 后指针p初始化指向首元结点 while(p) { // 后指针p指向空时结束 q = p->next; // 前指针q先去下一个结点 free(p); // 后指针p释放当前结点 p = q; // 后指针p跟着前指针q去下一个结点 } (*L)->data = 0; // 头结点数据域清0 (*L)->prior = NULL; // 头结点prior域指向NULL (*L)->next = NULL; // 头结点next域指向NULL } /** * 取元素和查找元素的算法思路: * 1. 先判断参数合法性 * 2. 再依次遍历每个元素,加个if判断即可 * 由于不需要操作头指针本体,所以可以直接用虚拟头指针进行移动 */ // 取链表中的元素 Status GetElem(DuLinkList L, int i, Elemtype* e) { if(IsEmpty(L) || i > GetLength(L) || i < 1) { // 链表长度为0、越界访问都是非法的 return ERROR; } int j; for(j = 1, L = L->next; L; j++) { if(j == i) { *e = L->data; return OK; } L = L->next; // 传值不改变头指针本体,可以直接拿虚拟的头指针L来移动 } return ERROR; } // 查找链表中的元素 int LocateElem(DuLinkList L, Elemtype e) { if(IsEmpty(L)) { // 空表不能查找 return 0; } int i; for(i = 1, L = L->next; L; i++) { if(L->data == e) { return i; } L = L->next; // 传值不改变头指针本体,可以直接拿虚拟的头指针L来移动 } return 0; // 如果没找到,返回0 } /** * 插入元素和删除元素的算法思路: * 1. 先判断参数的合法性 * 2. 再依次遍历每个元素,找到待操作结点(双向链表就不必找前驱了) * 如果要插入元素,需要拿一个新指针去申请结点,然后再进行逻辑上的插入操作 * 如果要删除元素,也需要哪一个新指针去保存要删除元素的地址,然后再进行逻辑上的删除操作, * 以免在进行逻辑上的删除操作后丢失地址 */ // 向链表中插入元素 Status Listlnsert(DuLinkList* L, int i, Elemtype e) { if(i < 1 || i > GetLength(*L)+1){ // 越界访问是非法的(+1是指可以插在最尾部) return ERROR; } int j; DuLinkList p, s; for(j = 1, p = (*L)->next; p; j++) { if(j == i-1) { // 找到新结点的前驱 s = (DuLinkList)malloc(sizeof(DuLNode)); // 申请新结点 if(!s){ return OVERFLOW; } s->data = e; // 新结点数据域赋值e s->next = p->next; // 新结点next域指向当前结点的后继 s->prior = p; // 新结点prior域指向当前结点 if(i != GetLength(*L)+1) { // 防止尾部空指针异常 p->next->prior = s; // 当前结点后继prior域指向新结点 } p->next = s; // 当前结点next域指向新结点 (*L)->data++; return OK; } p = p->next; } return ERROR; } // 从链表中删除元素 Status ListDelete(DuLinkList* L, int i, Elemtype* e) { if(IsEmpty(*L) || i < 1 || i > GetLength(*L)) { // 链表长度为0、越界访问都是非法的 return ERROR; } int j; DuLinkList p, q; for(j = 1, p = (*L)->next; p; j++) { if(j == i-1) { // 找到待删除结点的前驱 q = p->next; // q指针保存待删除结点地址,以免释放空间时地址丢失 *e = q->data; // e变量保存待删除结点的数据域 p->next = q->next; // 待删除结点前驱next域指向待删除结点后继 if(i != GetLength(*L)) { // 防止尾部空指针异常 q->next->prior = p; // 待删除结点后继prior域指向待删除结点前驱 } free(q); // 释放待删除结点的空间,完成删除操作 (*L)->data--; return OK; } p = p->next; } return ERROR; } /** * 头插法和尾插法建立链表的算法思路: * 头插法是不断申请新结点,将新结点插入到头结点和原有首元结点中间 * 尾插法是不断申请新结点,将新结点插入到尾部 * 两者算法上的最大区别是,头插法需要盯住头结点来进行逻辑操作,尾插法需要盯住尾结点进行操作 * 头结点是不会变的,而尾结点是一直在变的,所以尾插法需要在插入后更新尾结点的指针 */ // 头插法建立测试链表 Status CreateTestListByHead(DuLinkList* L) { ClearList(L); int i; DuLinkList p; for(i = 1; i <= 20; i++) { p = (DuLinkList)malloc(sizeof(DuLNode)); // p指针申请新结点 if(!p) { return OVERFLOW; } p->data = i; p->next = (*L)->next; // 新结点next域指向头结点的后继 p->prior = *L; // 新结点prior域指向头结点 if(i != 1) { // 防止尾部空指针异常 (*L)->next->prior = p; // 头结点后继prior域指向新结点 } (*L)->next = p; // 头结点next域指向新节点 (*L)->data++; } return OK; } // 尾插法建立测试链表 Status CreateTestListByTail(DuLinkList* L) { ClearList(L); int i; DuLinkList p, q; for(i = 1, q = *L; i <= 20; i++) { p = (DuLinkList)malloc(sizeof(DuLNode)); // p指针申请结点空间 if(!p) { return OVERFLOW; } p->data = i; p->next = NULL; // 新结点next域指向NULL(尾结点的后继) p->prior = q; // 新结点prior域指向原尾结点 q->next = p; // 尾结点next域指向新节点 q = p; // 新结点成为尾结点 (*L)->data++; } return OK; } // 合并链表 DuLinkList Connect(DuLinkList Ta, DuLinkList Tb) { if(IsEmpty(Ta) || IsEmpty(Tb)) { // 如果Ta和Tb有一个是空表,就返回那个不是空表的 return Ta?Ta:Tb; // 如果都是空表,显然就返回空表 } DuLinkList p; p = Ta; while(p->next) { // p指针最终指向尾结点 p = p->next; } p->next = Tb->next; // Ta链表尾部接到Ta链表头部(首元结点),完成合并 Tb->next->prior = p; Ta->data += Tb->data; // 链表长度相加 free(Tb); // 释放原Tb链表的头结点 return Ta; // Ta指针指向原Ta链表的头部,此时也是合并链表的头部 } // 展示测试链表中的所有元素 void ShowList(DuLinkList L) { DuLinkList p; if(!L) { // L已销毁的情况 printf("链表已销毁!\n"); return; } else if(IsEmpty(L)) { // L为非空表的情况 printf("[]\n"); } else { // L为空表的情况 p = L->next; printf("["); while(p->next) { // 最后一位单独输出 printf("%d, ", p->data); p = p->next; } printf("%d]\n", p->data); // 最后一位单独输出 } printf("length = %d\n", GetLength(L)); }
-
测试
#include<stdio.h> #include"DuLinkList.h" int main() { Elemtype e; DuLinkList L, Ta, Tb, T; // 测试InitList(&L) printf("InitList(&L)"); getchar(); InitList(&L); ShowList(L); getchar(); // 测试长度为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试CreateTestListByHead(&L) printf("CreateTestListByHead(&L)"); getchar(); CreateTestListByHead(&L); ShowList(L); getchar(); // 测试长度不为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试ClearList(&L) printf("ClearList(&L)"); getchar(); ClearList(&L); ShowList(L); getchar(); // 测试CreateTestListByTail(&L) printf("CreateTestListByTail(&L)"); getchar(); CreateTestListByTail(&L); ShowList(L); getchar(); // 测试LocateElem(L, 10) printf("LocateElem(L, 10)"); getchar(); printf("%d\n", LocateElem(L, 10)); getchar(); // 测试Listlnsert(&L, 5, 111) printf("Listlnsert(&L, 5, 111)"); getchar(); Listlnsert(&L, 5, 111); ShowList(L); getchar(); // 测试Listlnsert(&L, 22, 222) printf("Listlnsert(&L, 22, 222)"); getchar(); Listlnsert(&L, 22, 222); ShowList(L); getchar(); // 测试ListDelete(&L, 3, &e); printf("ListDelete(&L, 3, &e)"); getchar(); ListDelete(&L, 3, &e); ShowList(L); printf("%d\n", e); getchar(); // 测试ListDelete(&L, 21, &e) printf("ListDelete(&L, 21, &e)"); getchar(); ListDelete(&L, 21, &e); ShowList(L); printf("%d\n", e); getchar(); // 测试DestroyList(&L) printf("DestroyList(&L)"); getchar(); DestroyList(&L); ShowList(L); // 测试Connect(LinkList Ta, LinkList Tb) InitList(&Ta); InitList(&Tb); CreateTestListByHead(&Ta); CreateTestListByHead(&Tb); getchar(); printf("Ta: "); getchar(); ShowList(Ta); getchar(); printf("Tb: "); getchar(); ShowList(Tb); getchar(); printf("Connect(LinkList Ta, LinkList Tb)\n"); getchar(); T = Connect(Ta, Tb); ShowList(T); system("pause"); }
双向循环链表
-
双向循环链表操作的实现
#include<stdio.h> #include<stdlib.h> // 定义布尔值 #define TRUE 1 #define FALSE 0 // 定义状态码 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status; typedef int boolean; typedef int Elemtype; // Elemtype类型可以根据需求改动 typedef struct Node { Elemtype data; // 数据域 struct Node *prior, *next; // 指针域(带有递归的思想) }Node, *DuCirLinkList; // DuCirLinkList是Node的指针类型 // 通过头指针控制双向循环链表 // 初始化链表 Status InitList(DuCirLinkList *L) { *L = (DuCirLinkList)malloc(sizeof(Node)); // 申请头结点 if(!(*L)) { return OVERFLOW; } (*L)->data = 0; // 头结点数据域初始化为0 (*L)->next = *L; // 头结点next域指向自身 (*L)->prior = *L; // 头结点prior域指向自身 return OK; } // 求链表的长度 int GetLength(DuCirLinkList L) { int i; DuCirLinkList p; // 头指针需要作为结束标志,因此需要用新指针来移动 for(i = 0, p = L; p != L->prior; i++) { p = p->next; } return i; } // int GetLength(DuCirLinkList R) { // return L->data; // 头结点数据域就存放着链表的长度,可以直接调用 // } // 判断链表是否为空 boolean IsEmpty(DuCirLinkList L) { if(L != L->next) { // 头结点next域指的不是本身就非空 return FALSE; } else { return TRUE; } } /** *销毁链表和清空链表的算法思路: *“过河拆桥”算法,采用前后指针 *前指针先去下一个结点,后指针释放当前结点,再跟着前指针去下一个结点 *直到前指针指向尾结点,然后后指针跟上也指向头结点时结束 *区别是销毁链表要连头结点一起销毁掉,清空链表要保留头结点 */ // 销毁链表 void DestroyList(DuCirLinkList *L) { DuCirLinkList p, q; // q作为前指针,p指针作为后指针(头指针需要作为结束标志) p = (*L)->next; // 后指针p初始化指向首元结点 while(p != *L) { // 后指针p指向头结点时结束 q = p->next; // 前指针*L先去下一个结点 free(p); // 后指针p释放当前结点 p = q; // 后指针p跟着前指针*L去下一个结点 } free(*L); // 释放头结点 *L = NULL; } // 清空链表 void ClearList(DuCirLinkList *L) { if(IsEmpty(*L)) { // 空表不用清空 return; } DuCirLinkList p, q; // q作为前指针,p指针作为后指针(头指针需要作为结束标志) p = (*L)->next; // 后指针p初始化指向首元结点 while(p != (*L)) { // 后指针p指向头结点时结束 q = p->next; // 前指针*L先去下一个结点 free(p); // 后指针p释放当前结点 p = q; // 后指针p跟着前指针*L去下一个结点 } (*L)->data = 0; // 头结点数据域清0 (*L)->next = *L; // 头结点prior域指向自身 (*L)->prior = *L; // 头结点next域指向自身 } /** *取元素和查找元素的算法思路: *1. 先判断参数合法性 *2. 再依次遍历每个元素,加个if判断即可 */ // 取链表中的元素 Status GetElem(DuCirLinkList L, int i, Elemtype *e) { if(IsEmpty(L) || i > GetLength(L) || i < 1) { // 链表长度为0、越界访问都是非法的 return ERROR; } int j; DuCirLinkList p; // 头指针需要作为结束标志,因此需要用新指针来移动 for(j = 1, p = L->next; p != L; j++) { if(j == i) { *e = p->data; return OK; } p = p->next; } return ERROR; } // 查找链表中的元素 int LocateElem(DuCirLinkList L, Elemtype e) { if(IsEmpty(L)) { // 空表不能查找 return 0; } int i; DuCirLinkList p; // 尾指针需要作为结束标志,因此需要用新指针来移动 for(i = 1, p = L->next; p != L; i++) { if(p->data == e) { return i; } p = p->next; } return 0; // 如果没找到,返回0 } /** *插入元素和删除元素的算法思路: *1. 先判断参数的合法性 *2. 再依次遍历每个元素,找到待操作结点的前驱 * 如果要插入元素,需要拿一个新指针去申请结点,然后再进行逻辑上的插入操作 * 如果要删除元素,也需要哪一个新指针去保存要删除元素的地址,然后再进行逻辑上的删除操作, *以免在进行逻辑上的删除操作后丢失地址 * 找前驱的目的是方便逻辑上的操作,用新指针的目的是方便物理上的操作 */ // 向链表中插入元素 Status Listlnsert(DuCirLinkList *L, int i, Elemtype e) { if(i < 1 || i > GetLength(*L)+1){ // 越界访问是非法的(+1是指可以插在最尾部) return ERROR; } int j, insertInTail; DuCirLinkList p, s; insertInTail = 0; if(i == GetLength(*L)+1) { // 如果插在最尾部,后面需要移动尾指针 insertInTail = 1; } for(j = 1, p = (*L)->next; p != *L; j++) { if(j == i-1) { // 找到新结点的前驱 s = (DuCirLinkList)malloc(sizeof(Node)); // 申请新结点 if(!s){ return OVERFLOW; } s->data = e; // 新结点数据域赋值e s->next = p->next; // 新结点next域指向当前结点的后继 s->prior = p; // 新结点prior域指向当前结点 p->next->prior = s; // 当前结点后继prior域指向新结点(双向循环链表没有空指针异常) p->next = s; // 当前结点next域指向新结点 (*L)->data++; return OK; } p = p->next; } return ERROR; } // 从链表中删除元素 Status ListDelete(DuCirLinkList *L, int i, Elemtype *e) { int j, deleteInTail; DuCirLinkList p, q; if(IsEmpty(*L) || i < 1 || i > GetLength(*L)) {// 链表长度为0、越界访问都是非法的 return ERROR; } deleteInTail = 0; if(i == GetLength(*L)) { // 如果在最尾部删除,后面需要移动尾指针 deleteInTail = 1; } for(j = 1, p = (*L)->next; p != *L; j++) { if(j == i-1) { // 找到待删除结点的前驱 q = p->next; // q指针保存待删除结点地址,以免释放空间时地址丢失 *e = q->data; // e变量保存待删除结点的数据域 p->next = q->next; // 待删除结点前驱next域指向待删除结点后继 q->next->prior = p; // 待删除结点后继prior域指向待删除结点前驱 //(双向循环链表没有空指针异常) free(q); // 释放待删除结点的空间,完成删除操作 (*L)->data--; return OK; } p = p->next; } return ERROR; } /** *头插法和尾插法建立链表的算法思路: *头插法是不断申请新结点,将新结点插入到头结点和原有首元结点中间 *尾插法是不断申请新结点,将新结点插入到尾部 *两者算法上的最大区别是,头插法需要盯住头结点来进行逻辑操作,尾插法需要盯住尾结点进行操作 *头结点是不会变的,而尾结点是一直在变的,所以尾插法需要在插入后更新尾指针 */ // 头插法建立测试链表 Status CreateTestListByHead(DuCirLinkList *L) { ClearList(L); int i; DuCirLinkList p; for(i = 1; i <= 20; i++) { p = (DuCirLinkList)malloc(sizeof(Node)); // p指针申请新结点 if(!p) { return OVERFLOW; } p->data = i; p->next = (*L)->next; // 新结点next域指向头结点的后继 p->prior = *L; // 新结点prior域指向头结点 (*L)->next->prior = p; // 头结点后继prior域指向新结点(双向循环链表没有空指针异常) (*L)->next = p; // 头结点next域指向新节点 (*L)->data++; } return OK; } // 尾插法建立测试链表 Status CreateTestListByTail(DuCirLinkList *L) { ClearList(L); int i; DuCirLinkList p; for(i = 1; i <= 20; i++) { p = (DuCirLinkList)malloc(sizeof(Node)); // p指针申请结点空间 if(!p) { return OVERFLOW; } p->data = i; p->next = *L; // 新结点next域指向头结点(尾结点的后继) p->prior = (*L)->prior; // 新结点prior域指向原尾结点 (*L)->prior->next = p; // 尾结点next域指向新节点 (*L)->prior = p; // 头结点(尾结点的后继)prior域指向新节点 (*L)->data++; } return OK; } // 合并链表 DuCirLinkList Connect(DuCirLinkList Ta, DuCirLinkList Tb) { if(IsEmpty(Ta) || IsEmpty(Tb)) { // 如果Ta和Tb有一个是空表,就返回那个不是空表的 return Ta?Ta:Tb; // 如果都是空表,显然就返回空表 } Tb->prior->next = Ta->next; // Tb链表尾部接到Ta链表头部(首元结点) Ta->next->prior = Tb->prior; Ta->prior->next = Tb; // Ta链表尾部接到Tb链表头部(头结点),完成合并 Tb->prior = Ta->prior; Tb->data += Ta->data; // 链表长度相加 free(Ta); // 释放原Ta链表的头结点 return Tb; // Tb指针指向原Tb链表的头部,此时也是合并链表的头部 } // 展示测试链表中的所有元素 void ShowList(DuCirLinkList L) { DuCirLinkList p; if(!L) { // R已销毁的情况 printf("链表已销毁!\n"); return; } else if(IsEmpty(L)) { // L为空表的情况 printf("[]\n"); } else { // L为非空表的情况 p = L->next; printf("["); while(p != L->prior) { // 最后一位单独输出 printf("%d, ", p->data); p = p->next; } printf("%d]\n", p->data); // 最后一位单独输出 } printf("length = %d\n", L->data); }
-
测试
#include<stdio.h> #include"DuCirLinkList.h" int main() { Elemtype e; DuCirLinkList L, Ta, Tb, T; // 测试InitList(&L) printf("InitList(&L)"); getchar(); InitList(&L); ShowList(L); getchar(); // 测试长度为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试CreateTestListByHead(&L) printf("CreateTestListByHead(&L)"); getchar(); CreateTestListByHead(&L); ShowList(L); getchar(); // 测试长度不为0时GetLength(L) printf("GetLength(L)"); getchar(); printf("%d\n", GetLength(L)); getchar(); // 测试ClearList(&L) printf("ClearList(&L)"); getchar(); ClearList(&L); ShowList(L); getchar(); // 测试CreateTestListByTail(&L) printf("CreateTestListByTail(&L)"); getchar(); CreateTestListByTail(&L); ShowList(L); getchar(); // 测试LocateElem(R, 10) printf("LocateElem(L, 10)"); getchar(); printf("%d\n", LocateElem(L, 10)); getchar(); // 测试Listlnsert(&L, 5, 111) printf("Listlnsert(&L, 5, 111)"); getchar(); Listlnsert(&L, 5, 111); ShowList(L); getchar(); // 测试Listlnsert(&L, 22, 222) printf("Listlnsert(&L, 22, 222)"); getchar(); Listlnsert(&L, 22, 222); ShowList(L); getchar(); // 测试ListDelete(&L, 3, &e); printf("ListDelete(&L, 3, &e)"); getchar(); ListDelete(&L, 3, &e); ShowList(L); printf("%d\n", e); getchar(); // 测试ListDelete(&L, 21, &e) printf("ListDelete(&L, 21, &e)"); getchar(); ListDelete(&L, 21, &e); ShowList(L); printf("%d\n", e); getchar(); // 测试DestroyList(&L) printf("DestroyList(&L)"); getchar(); DestroyList(&L); ShowList(L); // 测试Connect(LinkList Ta, LinkList Tb) InitList(&Ta); InitList(&Tb); CreateTestListByHead(&Ta); CreateTestListByHead(&Tb); getchar(); printf("Ta: "); getchar(); ShowList(Ta); getchar(); printf("Tb: "); getchar(); ShowList(Tb); getchar(); printf("Connect(LinkList Ta, LinkList Tb)\n"); getchar(); T = Connect(Ta, Tb); ShowList(T); system("pause"); }
顺序表和链表的比较
-
操作算法分析
-
操作算法分析
- 时间复杂度:查找、插入、删除算法的平均时间复杂度为 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)
- 空间复杂度:操作算法的空间复杂度 S ( n ) = O ( 1 ) S(n) = O(1) S(n)=O(1),(没有占用辅助空间)
-
链表操作算法分析
单链表、循环链表和双向链表的时间效率比较
查找首元结点 查找表尾结点 查找当前结点的前驱结点 带头结点设头指针L的单链表 L->next
;时间复杂度 O ( 1 ) O(1) O(1)从 L->next
依次向后遍历;时间复杂度 O ( n ) O(n) O(n)通过 p->next
依次向后遍历无法找到前驱带头结点设头指针L的循环链表 L->next
;时间复杂度 O ( 1 ) O(1) O(1)从 L->next
依次向后遍历;时间复杂度 O ( n ) O(n) O(n)通过 p->next
依次向后遍历可以找到前驱;时间复杂度 O ( n ) O(n) O(n)带头结点设尾指针R的循环链表 R->next->next
;时间复杂度 O ( 1 ) O(1) O(1)R
;时间复杂度 O ( 1 ) O(1) O(1)通过 p->next
可以找到前驱;时间复杂度 O ( n ) O(n) O(n)带头结点设头指针L的双向链表 L->next
;时间复杂度 O ( 1 ) O(1) O(1)L->prior
;时间复杂度 O ( 1 ) O(1) O(1)p->prior
;时间复杂度 O ( 1 ) O(1) O(1)
-
-
优缺点分析
- 顺序表的优缺点
- 顺序存储结构的优点
- 存储密度大(结点本身所占存储量/结点结构所占存储量)
- 可以随机存取表中任一元素
- 顺序存储结构的缺点
- 在插入、删除某一元素时,需要移动大量元素
- 浪费存储空间
- 属于静态存储形式,数据元素的个数不能自由扩充
- 顺序存储结构的优点
- 链表的优缺点
- 链式存储结构的优点
- 结点空间可以动态申请和释放;
- 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。
- 链式存储结构的缺点
- 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。
- 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。
- 链式存储结构的优点
- 顺序表的优缺点
线性表的应用
-
并集
/**线性表的合并 * 问题描述: * 假设利用两个线性表La和Lb分别表示两个集合A和B,现要求一个新的集合A = A U B */ #include<stdio.h> #include"SqList.h" #include"LinkList.h" // typedef SqList List; typedef LinkList List; List arrayToList(Elemtype *array, int array_len) { int i; List L; InitList(&L); for(i = 1; i <= array_len; i++) { Listlnsert(&L, i, array[i-1]); } return L; } List ListUnion(List A, List B) { int i, elem; List C; C = A; for(i = 1; i <= GetLength(B); i++) { // 遍历B表 GetElem(B, i, &elem); if (!(LocateElem(C, elem))) { // 如果B表中当前元素在A表中没有 Listlnsert(&C, GetLength(C)+1, elem); // 那就加到A表中 } } return C; } int main() { List A, B, C; int A_array[] = {7, 5, 3}; int B_array[] = {11, 2, 6, 3}; A = arrayToList(A_array, 3); B = arrayToList(B_array, 4); ShowList(A); ShowList(B); C = ListUnion(A, B); ShowList(C); }
-
有序表的合并
/** * 已知线性表La和Lb中的数据元素按值非递减有序排列 * 现要求将La和Lb归并为一个新的线性表Lc,且Lc中的数据元素仍按值非递减有序排列 */ #include<stdio.h> #include"SqList.h" SqList arrayToSqList(Elemtype *array, int array_len) { int i; SqList L; InitList(&L); for(i = 1; i <= array_len; i++) { Listlnsert(&L, i, array[i-1]); } return L; } SqList SequenceMerge(SqList A, SqList B) { Elemtype *pa, *pb, *pc, *pa_last, *pb_last; SqList C; InitList(&C); pa = A.elem; // pa指针用于遍历A表 pb = B.elem; // pb指针用于遍历B表 pc = C.elem; // pc指针用于遍历C表 pa_last = pa + GetLength(A) - 1; // pa_last指针用于指示A表结束位置 pb_last = pb + GetLength(B) - 1; // pb_last指针用于指示B表结束位置 while(pa <= pa_last && pb <= pb_last) { // pa、pb指针不断遍历,直到有一个表遍历完全 if(*pa <= *pb) { *pc++ = *pa++; // 如果A表元素小,就将其加到C表中,同时指针后移一格 } else { *pc++ = *pb++; // 如果B表元素小,就将其加到C表中,同时指针后移一格 } C.length++; } while(pa <= pa_last) { // 如果A表还没遍历完 *pc++ = *pa++; // 就将A表后续所有元素加到C表中 C.length++; } while(pb <= pb_last) { // 如果B表还没遍历完 *pc++ = *pb++; // 就将B表后续所有元素加到C表中 C.length++; } return C; } int main() { SqList A, B, C; int A_array[] = {1, 7, 8}; int B_array[] = {2, 4, 6, 8, 10, 11}; A = arrayToSqList(A_array, 3); ShowList(A); B = arrayToSqList(B_array, 6); ShowList(B); C = SequenceMerge(A, B); ShowList(C); return 0; }