数据结构第四篇——(一般)线性表(基于C语言)
Posted 从零开始的智障生活
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构第四篇——(一般)线性表(基于C语言)相关的知识,希望对你有一定的参考价值。
前言
以下描述内容以C语言为主,Java只是作为实现的补充。
一、线性表的定义及性质
线性表是由n(n>=O)个数据类型相同的元素构成的有限序列。
线性表中元素的个数n(n>=O)定义为表长,n=O时称为空表。
线性表按照存储结构的不同,又划分为顺序表和链式表。
1.1 顺序表的定义及其特点
顺序表(Sequential List):用一组地址连续的存储单元依次存储线性表的数据元素。
其特点是:
- 逻辑上相邻的数据元素,其物理次序也是相邻的;
- 随机访问,可以在O(1)时间内找到第i个元素;
- 存储密度高,每个结点只存储数据元素;
- 扩展容量不方便(即使采用动态分配的方式,拓展长度的时间复杂度都很高)。
假设线性表的每个元素需占用T个存储单元,并以占用的第一个单元的存储地址作为数据元素的起始位置。线性表中第i+1个元素的存储位置LOC(ai+1)和第i个数据元素的存储位置LOC(ai)之间的关系是LOC(ai+1)= LOC(ai)+T。线性表的第i个数据元素ai的存储位置为:LOC(ai)=LOC(a1)+(i-1)*T。但指针只需要*(p+1)。
1.2 链式表的定义及其特点
链式表(Linked List):用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续或非连续的)。
特点:
- 逻辑上相邻的数据元素,物理上不用相邻。
- 顺序访问结构,访问任意元素的平均时间复杂度是O(n)
- 存储密度低,每个结点包括数据域和指针域。
- 插入删除的时间复杂度低。
二、线性表的逻辑结构,以及基本操作
用抽象数据类型,来描述线性表的逻辑结构
三、线性表的抽象数据类型的实现
3.1 用顺序存储结构对线性表基本操作的实现
先拆分开来看,完整代码会在后面给出。
3.1.1 创建与初始化基于顺序存储结构的线性表
顺序表的初始化:分为静态分配和动态分配 参考
静态分配:用静态的“数组”存放数据元素;缺点:顺序表大小容量不可修改,容易浪费内存资源或造成溢出。
补充,用Java实现,其实就是数组,没什么要补充的。
import java.util.Arrays;
/**
* @author zyx
* 用静态分配的顺序存储结构创建线性表
*/
public class SLOfStaticAllocated {
private final static int MaxSize = 10;
private int[] data = new int[MaxSize]; // 创建数组,并分配内存空间。
private int length = 0; // 数组长度
// 初始化线性表
SLOfStaticAllocated() {
Arrays.fill(data, 0);// 全部填充为0
}
}
动态分配:即用指针方式指向一片连续区域的首地址。
3.1.2 实现基于顺序存储结构的线性表基本功能
扩展表
| int IncreaseList(SqList *L,int len){ int *p = L->data; //指针指向一块新的特定大小的区域 L->data = (int *)malloc((L->MaxSize+len)*sizeof(int)); if(!L->data){ printf("容量增加失败\\n"); exit(OVERFLOW); } int i = 0; while(i!=L->length){ L->data[i] = p[i]; i++; } L->MaxSize = L->MaxSize+len; free(p); //释放原内存块 return 1; } |
插入 1<=i<=L->lenght+1,在第i个结点前插入节点e,平均时间复杂度为O(n) 边界条件:
不合法条件:
注意最后如果插入成功,那么length+1 | int ListInsert(SqList *L,int i,int e){ // 1.判断插入位置i是否合法(i值的合法范围是l<=i<=L->length+1), 若不合法 则返回 ERROR。 if(i<1||i>L->length+1){ printf("i值不合法\\n"); return ERROR; //i值不合法 } // 2.判断顺序表的存储空间是否已满,若满见肤返回 ERROR。 if(L->length == L->MaxSize) { printf("存储空间已满\\n"); return ERROR; //存储空间已满 } // 3.将第n个至第i个位置的元素依次向后移动一个位置,特别地空出第l个位置(i=L->length+l 时无需移动)。 for (int j = L->length; j >=i ; j--)// L->data[L->length] = L->data[L->length-1] { L->data[j] = L->data[j-1]; } L->data[i-1] = e; L->length++; return 1; } |
删除结点 删除第i个元素,平均时间复杂度是O(n) | int ListDelete(SqList *L,int i){ // 1.判断删除位置i(1<=i<=L->length)是否合法 if(i<1||i>L->length) return ERROR; // 2. 将第i+1个元素至第L->length个元素,依次向前移动一个位置(第L->length个元素时无需移动)。 for (int j = i; j < L->length; ++j) { L->data[j-1] = L->data[j]; } // 3.表的长度减1 L->length--; return 1; } |
根据位置取值 | int GetElem(SqList L,int i,int *e){ // 1.判断查找位置i(1<=i<=L.length)是否合法 if(i<1||i>L.length) return ERROR; *e = L.data[i-1]; return 1; } |
根据关键值索引 | int LocateElem(SqList L,int e){ for (int i = 0; i < L.length; ++i) { if(e==L.data[i]) return i+1; } return 0; // 查找失败,未找到 } |
列表逆序 | void ReverseList(SqList *L) { if (L->length) for (int i = 0; i<L->length - 1 - i; i++) { int t = L->data[i]; L->data[i] = L->data[L->length - 1 - i]; L->data[L->length - 1 - i] = t; } } |
列表长度 | int Length(SqList L){ return L.length; } |
列表容量 | int ListSize(SqList L){ return L.MaxSize; } |
列表是否为空 | boolean Empty(SqList L){ return L.length==0; } |
销毁列表 顺序表因为是数据域data就代表整个区域,所以只要free(L) 即可。 | int DestroyList(SqList *L){ free(L); if(!L->data) return 1; else return ERROR; } |
遍历列表 | int TravelList(SqList L){ for (int i = 0; i < L.length; ++i) { printf("%d\\n",L.data[i]); } return 1; } |
用顺序存储结构实现线性表其实比较少,对于一些比较复杂的线性表结构,更多的是用链式存储结构实现。
3.2 用链式存储结构的实现技术,比如单向链表、双向链表、单循环链表、双向循环链表以及带头节点的链表,以及其对线性表基本操作的实现
3.2.1 单链表
要表示一个单链表时只需声明一个头指针,指向单链表的第一个结点。
定义一个单链表结点类型 | typedef struct LNode // 定义单链表结点类型 { int data; // 每个结点存放一个数据元素 struct LNode *next; // 指针指向下一节点 }LNode,*LinkList; | LNode *L; // 声明一个指向单链表第一个结点的指针 LinkList L; // 声明一个指向单链表第一个结点的指针 // 这两个效果等价,但这种命名方式增加可读性 // LNode 强调结点,而LinkList强调单链表, // 返回值是LNode *强调返回单链表的一个结点 // 返回值是LinkList 强调返回一个单链表 |
初始化: 初始化一个单链表(无头结点) | bool InitList(LinkList L){ // 注意L是指针类型 L = NULL; // 空表,暂时无任何数据 return true; } | 头指针L->NULL 头指针指向的下一节点,如果存在的话就是具有有效数据的结点。 空表判断L==NULL |
初始化: 初始化一个单链表(有头结点) | bool InitList(LinkList L){ // 生成新节点作为头结点,用头指针L指向头结点 L = (LNode *)malloc(sizeof(LNode)); if(L==NULL) return false; L->next = NULL; // 头结点的指针域置空 return true; } | 头指针指向头结点,但头结点的数据没有任何实际价值,头结点的下一节点才是具有有效数据的结点。 空表判断L->next==NULL |
插入节点: 按位序前插(带头结点): 将值为e的新节点插入到表第i个结点的位置上,即插入到第ai个结点前面。 | bool ListInsert(LinkList L,int i,int e){ if(i<1) return false; LNode *p; // 指针p指向扫描到的结点 int j = 0; //当前p指向的是第几个结点 p = L; // L指向头结点,头结点是第0个结点(不存在数据) while(p!=NULL && j<i-1){// 扫描到第i-1个元素且当前节点不为空 p = p->next; j++ } if(p==NULL) return false; // i值不合法 LNode *s = (LNode *)malloc(sizeof(LNode)); // 为新节点申请空间 s->data = e; s->next = p->next; p->next = s; return true; } | 从上面带头结点的初始化可以看出,传入的参数LinkList L是代表链表,也代表头结点。 提示:这里结点的数据是存放在某一地址中,地址只能由指针指向,所以结点的数据类型只能是结点指针。 而结点指针指向的只是一个结点空间的地址,而这个地址存放该节点的数据和下一个结点的地址。 为什么用指针,而不直接用一个结构体变量定义结点? 如果用非指针变量a定义结点,那么这就不是表了,而是一个个独立的数据元素,而且,指针p,p->next = &a;或p->next =a效果一样,但是这样一个100 项的列表要定义100个变量,很不方便。 |
插入节点: 指定结点的后插 | bool InsertNextNode(LNode *p,int e){ if(p==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); if (s==NULL) return false; s->data = e; s->next = p->next; p->next = s; return true; } | 不用考虑是否带头结点 |
插入节点: 指定节点的前插操作 | bool InsertPriorNode(LNode *p,LNode *s){ if(p==NULL||s==NULL) return false;// 1.验证有效 s->next = p->next; // 2. 先进行后插 p->next = s; int temp = p->data; // 3. 将p结点数据与s结点数据交换 p->data = s->data; s->data = temp; return true } | 因为不知道前驱结点,而且两个结点反正是在同一链表上,交换结点,就等价于交换结点数据,所以只要后插,然后交换结点数据就可以了。 不用考虑是否带头结点 |
删除操作: 按位序删除删除 | bool ListDelete(LinkList L,int i,int *e){ if (i<1) return false; LNode *p; // 指针p扫描到当前节点 int j = 0; // j表示到第几个结点 P = L; // 找到要删除结点的前驱结点 while(p!=NULL && j<i-1){ p = p->next; j++; } if(p==NULL) return false; // 将前驱结点与后继结点连接 LNode *q = p->next; //令q指向被删除结点 *e = q->data; // e返回删除的值 p->next = q->next; // 将前驱结点与后继结点相连 free(q); // 释放删除结点内存 return true; } | 这里LinkList L和LNode *p=L的实际意义是一样的,但是这样做的意义是:L代表当前链表,p代表节点指针,用来指向当前节点,增加可读性。 删除结点一定要保证:链表不断; 链表不断一定要保证:前驱结点与后继结点相连。 |
删除结点: 指定节点删除 | bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q = p->next; // 用q指向后继结点 if(q==NULL){//如果p是尾结点,那么p->next==NULL p = NULL; return true; } p->data = p->next->data; // 和后继节点交换数据 p->next = q->next; // 将后继结点从链表中剔除 free(q); // 释放后继结点的存储空间; return true; } | 难点在于,只有一个要删除的结点,没有前驱结点,删除当前节点,后面的就没了。 思想:将删除结点等价为删除结点数据。 注意:为了保证链表不断,需要交换结点,只要将后继结点的数据放到前面,不用交换,因为要删除,里面是什么都不用考虑。 优点在于时间复杂度为O(1) |
查找: 按位查找,返回第i个元素(带头结点) | LNode *GetElem(LinkList L,int i){ if(i<0) return NULL; LNode *p; j = 0; p = L; while(p!=NULL && j<i){ p = p->next; j++; } return p; } | 注意:考虑边界情况,增加代码健壮性,如i<0或i超过链表长度。 平均时间复杂度:O(n) |
查找: 按值查找,找到数据域==e的结点 | LNode *LocateElem(LinkList L,int e){ LNode *p = L; while(p!=NULL && p->data != e){ p = p->next; } return p; } |
|
求表长 | int Length(LinkList L){ int len = 0; LNode *p = L; while(p->next!=NULL){ p = p->next len++; } return len; } | |
建表: 尾插法 | LinkList ListTailInsert(LinkList L){ int x;// 新节点的数据 L = (LinkList)malloc(sizeof(LNode));//建立头结点 初始化空表 // LNode *s,*t = L;//s表示新的结点,t表示尾结点 LNode *s; LNode *t = L; scanf("%d",&x); while(x!=-1){ s = (LNode *)malloc(sizeof(LNode));// 为新节点分配空间 t->next = s; s->data = x; t = s; scanf("%d",&x); } t->next = NULL; //尾结点置空 return L; } | 尾插法建立单链表,是正向建立单链表,比起每次尾插一个结点,循环一次,时间复杂度为O(n^2),不如定义一个尾结点,用来标记尾巴。 这里面是将链表初始化的过程包含了。时间复杂度是O(n) |
建表: 头插法 | LinkList ListHeadInsert(LinkList L){ int x; LNode *s; L = (LinkList)malloc(sizeof(LNode)); L->next = NULL; scanf("%d",&x); while(x!=-1){ s = (LNode *)malloc(sizeof(LNode));//为新节点分配空间 s->data = x; s->next = L->next; L->next = s; scanf("%d",&x); } return L; } | 也就是每次对头结点进行后插操作。 注意:养成好习惯,只要是初始化单链表,都先把头指针指向NULL,不一定有影响,但是可以防止在没有初始化的情况下发生意外。 |
逆序单链表(带头结点) | LinkList ListReverse(LinkList L){ // 1. 空链表,或者只有一个结点的链表 if(L==NULL||L->next==NULL) return NULL; // 2. 按顺序将所有剩下的结点对【头结点的后一个结点L->next】进行头插 // 直到剩余结点为空 LNode *s = L->next;// 当前头结点的下一节点,之后的标志,不变 LNode *r = s->next; // 当前待头插的结点,一直成为L->next while(s->next!=NULL){ s->next = r->next; // H->A->B->C->D变成H->A->C->D,B r->next = L->next; // 再变成H->B->A->C->D L->next = r; // 下次循环H-B-A-C-D变成H-B-A-D,C // 再变成H-C-B-A-D } return L; } | 逆序头结点不作考虑,位置不变。如果L==NULL,那么只有空链表,如果L->next==NULL,那么就表示只有一个结点,那就不用逆序。 我们将L->next->next结点B,对结点L->next进行头插。 而s=L->next之后将作为标志来获取下一个待头插结点。 |
3.2.2 双链表
声明双链表类型 | typedef struct { int data; struct DNode *prior,*next; }DNode,*DLinkList; | DNode *等价于DlinkList; |
初始化双链表 | bool InitDLinkList(DLinkList L){ L = (DLinkList)malloc(sizeof(DNode)); if(L==NULL) return false; L->prior = NULL; L->next = NULL; return false; } | 判断双链表是否为空: L->next == NULL |
插入: 在结点*p后插入节点*s。 | bool InsertNextDNode(DNode *p,DNode *s){ if(p==NULL||s==NULL) return false; s->next = p->next;// 将*s结点插入到结点*p之后。 p->next->prior = s; s->prior = p; p->next = s; return true; } | |
删除: 删除p结点的后继结点 | 删除p结点的后继结点 bool DeleteNextDNode(DNode *p){ if(p==NULL) return false; DNode *q = p->next; if(q==NULL) return false; p->next = q->next; if(q->next != NULL) q->next->prior = p; free(q); return true; } | |
销毁链表 | bool DestroyDList(DLinkList L){ while(L->next!=NULL){ DeleteNextDNode(DLinkList L); } free(L);//释放头结点 L=NULL;头指针指向NULL return true; } | |
双链表的遍历: 后向遍历 | void BackTraveler(DNode *p){ while(p!=NULL){ printf("%d\\n",p->data); p=p->next; } } | |
双链表的遍历: 前向遍历(包括头结点) | void ForeTraveler(DNode *p){ while(p!=NULL){ printf("%d\\n",p->data); p = p->prior; } } | |
双链表的遍历: 前向遍历(不包括头结点) | void ForeTraveler2(DNode *p){ while(p->prior!=NULL){ printf("%d\\n",p->data); p = p->prior; } } |
3.2.3 循环单链表
声明结构体类型。 | typedef struct { int data; struct CNode *next; }CNode,*CirList; | |
初始化结构体 | bool InitCList(CirList L){ L = (CNode *)malloc(sizeof(CNode));//分配一个头结点 if(L==NULL) return false; L->next = L;// 尾结点指向头结点 return true; } | |
判断循环单链表是否为空 | bool Empty(CirList L){ CNode *p = L;//指向当前节点 if(p->next=p) return true; else return false; } | |
判断该节点是否为循环单链表尾结点 | bool IsTail(CirList L,CNode *p){ if(L==NULL||p==NULL) return false; if(p->next==L) return true; else return false; } | |
测试 | bool TestCDList(){ CDList L; InitCDList(L); ................ return ture; } |
3.2.4 循环双链表
声明循环单链表结构体类型 | typedef struct{ int data; struct CDNode *prior; struct CDNode *next; }CDNode,*CDList; | |
初始化 | bool InitCDList(CDList L){ L=(CDList)malloc(sizeof(CDNode)); if(L==NULL) return false; L->prior = L; L->next = L; } | |
判断为空 | bool Empty(CDList L){ if(L->next==L) return true; else return false; } | |
判断是否是循环双链表的尾结点 | bool IsTail(CDList L,CDNode *p){ if(p->next==L) return true; else return false; } | |
测试 | bool TestCDList(){ CDList L; InitCDList(L); ..... Return true; } | |
插入: 在p结点后插入s结点 | bool InsertNextCDNode(CDNode *p,CDNode *s){ p->next->prior = s; s->next = p->next; p->next = s; s->prior = p; return true } | 与普通双链表的区别是,不用考虑p结点的后继结点是NULL的问题。 |
删除: 删除结点p | bool DeleteCDNode(CDNode *p){ CDNode *q = p->prior; 数据结构第四篇——线性表的链式存储之双向链表 |