第2章:线性表
2.1 线性表的定义和基本操作
线性表是具有相同数据类型的n个数据元素的有限序列。n为表长,当n=0时该线性表是一个空表。a1是唯一的『第一个』数据元素,又称表头元素。An是唯一的『最后一个』数据元素,又称表尾元素。除第一个元素外,每个元素有且仅有一个直接前驱。除最后一个元素外,每个元素有且仅有一个直接后驱。
线性表的特点:
1) 表中元素个数有限。
2) 表中元素具有逻辑上的顺序性,在序列中个元素排序有其先后次序。
3) 表中元素都是数据元素,每个元素都是单个元素。
4) 表中的数据类型都相同。每一个元素占有相同大小的存储空间。
5) 表中元素具有抽象性。即讨论元素间的逻辑关系,不考虑元素究竟表示什么内容。
线性表是一种逻辑结构,表示元素间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念。
线性表的基本操作:
InitList(&L):初始化表。构造一个空的线性表。
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中的第i个位置的元素的值。
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
PrintList(L):输入操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
Destroy(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
基本操作的实现取决于采用哪一种存储结构,存储结构不同,算法的实现也不同。『&』表示C++中的引用。如果传入的变量是指针类型的变量,且在函数体内要对传入的指针进行改变,则将用到指针变量的引用。
2.2 线性表的顺序表示
线性表的顺序存储又称顺序表,是用一组地址连续的存储单元,依次存储线性表中的数据元素。顺序表的表中元素的逻辑顺序与物理顺序相同。
假定线性表的元素类型为ElemType,
线性表的顺序存储类型描述为
1 #define MaxSize 50 //定义线性表的最大长度 2 typedef struct { 3 ElemType data[MaxSize]; //顺序表的元素 4 int length; //顺序表的当前长度 5 }SqList; //顺序表的类型定义
动态分配线性表
1 #define InitSize 100 //表长度的初始定义 2 typedef struct { 3 ElemType *data; //指示动态分配数组的指针 4 int MaxSize, length; //数组的最大容量和当前个数 5 } SqList;
C的初始动态分配语句为
1 L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize);
C++的初始动态分配语句为
1 L.data = new ElemType[InitSize];
『1』插入操作
在顺序表L的第i(1<=i<=L.length+1)个位置插入新元素e。如果i的输入不合法,则返回false,表示插入失败。
否则将顺序表的第i个元素以及气候的所有元素右移一个位置,腾出一个空位置插入新元素e,顺序表的长度增加1,插入成功,返回true
1 bool ListInsert(SqList &L, int i, ElemType e) { 2 //本算法实现将元素e插入到顺序表L中的第i个位置 3 if(i<1||i>L.length+1) //判断i的范围是否有效 4 return false; 5 if(L.length >= MaxSize) //当前存储空间已满,不能插入 6 return false; 7 for(int j = L.length; j >= i; j++) { 8 L.data[j] = L.data[j-1]; 9 } 10 L.data[i-1] = e; 11 L.length++; 12 return true; 13 }
线性表插入算法的平均时间复杂度为O(n)。
『2』删除操作
删除顺序表L中的第i(1<=i<=L.length)个位置的元素,成功则返回true,并将被删除的元素用引用变量e返回,否则返回false
1 bool ListDelete(SqList &L, int i; ElemType &e) { 2 //本算法实现删除顺序表L中的第i个位置的元素 3 if(i<1||i>L.length) //判断i的范围是否有效 4 return false; 5 e = L.data[i-1]; //将被删除的元素赋值给e 6 for(int j = i; j < L.length; j++) { //将第i个位置之后的元素前移 7 L.data[j-1] = L.data[j]; 8 } 9 L.length--; //线性表长度减一 10 return true; 11 }
线性表删除操作的平均时间复杂度为O(n)
『3』按值查找(顺序查找)
在顺序表L中查找第一个元素值等于e的元素,并返回其位序
1 int LocateElem(SqList L, ElemType e) { 2 //本算法实现查找顺序表中值为e的元素,如果查找成功,返回元素位序,否则返回0 3 int i; 4 for(int i = 0; i < L.length; i++) { 5 if(L.data[i] == e) { 6 return i+1; 7 } 8 } 9 return 0; 10 }
线性表按值查找算法的平均时间复杂度为O(n)
单链表中结点类型的描述如下:
1 typedef struct LNode { //定义单链表结点类型 2 ElemType data; //数据域 3 struct LNode *next; //指针域 4 }LNode, *LinkList;
动态分配并不是链式存储,仍属于顺序存储结构,其物理结构没有变化,依然是随机存取,只是在分配的空间大小可以在运行时决定。
顺序表的最主要特点是酥记访问,即通过首地址和元素序号可以在O(1)的时间内找到指定的元素。
顺序表的存储密度高,每隔结点只存储数据元素。
顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素。
2.3 线性表的链式表示
线性表的链式存储又称单链表,是通过一组任意的存储单元来存储线性表中的数据元素。
单链表中借点类型的描述如下:
1 typedef struct LNode { //定义单链表结点类型 2 ElemType data; //数据域 3 struct LNode *next; //指针域 4 }LNode, *LinkList;
由于单链表的元素是离散地分布在存储空间中的,所以单链表是非随机存取的存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从表头开始遍历,依次查找。
『1』采用用头插法建立单链表
1 LinkList CreatList1(LinkList &L) { 2 //从表尾到表头逆向建立单链表L,每次均在头结点之后插入元素 3 LNode *s; 4 int x; 5 L = (LinkList)malloc(sizeof(LNode)); //创建头结点 6 L->next = NULL; //初始为空链表 7 scanf("d", &x); //输入结点的值 8 while (x != 9999) { //输入9999表示结束 9 s = (LNode)malloc(sizeof(LNode)); //创建新结点 10 s->data = x; 11 s->next = L->next; 12 L-next = s; //将新结点插入表中,L为头指针 13 scanf("d", &x); 14 } //while结束 15 return L; 16 }
采用头插法建立单链表,读入数据的顺序与生成链表中元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总的时间复杂度为O(n)
『2』采用尾插法建立单链表
1 LinkList CreatList2(LinkList &L) { 2 //从表头到表尾正向建立单链表L,每次均在表尾插入元素 3 int x; //设元素类型为整形 4 L = (LinkList)malloc(sizeof(LNode)); 5 LNode *s, *r = L; //r为表尾指针 6 scanf("%d", &x); //输入结点的值 7 while (x != 9999) { //输入9999表示结束 8 s = (LNode*)malloc(sizeof(LNode)); 9 s->data = x; 10 r->next = s; 11 r = s; 12 scanf("%d", &x); 13 } 14 r->next = NULL; //尾结点指针置空 15 return L; 16 }
与头插法时间复杂度相同,为O(n)
『3』按序号查找结点值
在单链表中从第一个结点出发,顺指针next域诸葛往下搜索,直到找到第i个结点为止,否则返回最后一个结点指针域NULL
1 LNode *GetElem(LinkList L, int i) { 2 //本算法取出单链表L(带头结点)中第i个位置的结点指针 3 int j = 1; //计数,初始为1 4 LNode *p = L->next //头结点指针赋给p 5 if (i == 0) 6 return L; //若i等于0,则返回头结点 7 if (i < 1) 8 return NULL; //若i无效,则返回NULL 9 while (p&&j<i) { //从第1个结点开始找,查找第i个结点 10 p = p->next; 11 j++; 12 } 13 return p; //返回第i个结点的指针,如果i大宇表长,p=NULL,直接返回p即可 14 }
『4』按值查找表结点
从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的值等于给定值e,
则返回该借点的指针;若整个单链表中没有这样的结点,则返回NULL。
1 LNode *LocateElem(LinkList L, ElemType e) { 2 //本算法查找单链表(带头结点)中数据域值等于e的结点操作指针,否则返回NULL 3 LNode *p = L->next; 4 while (p != NULL && p->data != e) //从第1个结点开始查找data域为e的结点 5 p = p->next; 6 return p; //找到返回该结点指针,否则返回NULl 7 }
『5』插入结点操作
插入操作是将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,
即第i-1个结点,再在气候插入新结点。
前插操作
1 p = GetElem(L,i-1; //查找插入位置的前驱结点 2 s->next = p->next; 3 p->next = s;
后插操作
1 s->next = p->next; //修改指针域,不能颠倒 2 p->next = s; 3 temp = p->data; //交换数据域部分 4 p->data = s->data; 5 s->data = temp;
『6』删除结点操作
1 p = GetElem(L,i-1); //查找删除位置的前驱结点 2 q = p->next; //令q指向被删除结点 3 p->next = q->next; //将*q结点从链中『断开』 4 free(q); //释放结点的存储空间
删除结点*p
删除结点*p的操作可以用删除结点*p的后继结点操作来实现,实质是将其后继结点的值赋予其自身,
然后删除后继结点,也能使得时间复杂度为O(1)
1 q=p->next; 2 p->data=p->next->data; 3 p->next=q->next; 4 free(q);
双链表:为克服单链表的访问某结点的前驱结点的时间复杂度为O(n)的缺点引入双链表,双链表结点中有两个指针prior和next,分别指向某前驱结点和后继结点。
双链表中结点类型的描述如下:
1 typedef struct DNode { //定义双链表结点类型 2 ElemType data; //数据域 3 struct DNode *prior, *next; //前驱和后继指针 4 }DNode, *DLinklist;
『1』双链表的插入操作
在双链表中p所指的结点之后插入结点*s
1 s->next=p->next; //将结点*s插入到结点*p之后 2 p->next->prior=s; 3 s->prior=p; 4 p->next=s;
『2』双链表的删除操作
1 p->next=q->next; 2 q->next->prior=p; 3 free(q);
循环链表:循环链表和单链表的区别在于表中最后一个结点的指针不是NULL,而改为指向头结点从而整个链表形成一个环。循环单链表的判断条件不是头结点的指针是否为空,而是它是否等于头指针。
循环双链表:在循环双链表中,头结点的prior指针指向表尾结点。
静态链表:静态链表接住数组来描述线性表的链式存储结构。指针域next是结点的相对地址(数组下标),又称游标。
静态链表结构类型的描述如下:
1 #define MaxSize 50 //静态链表的最大长度 2 typedef struct { //静态链表结构类型的定义 3 ElemType data; //存储数据元素 4 int next; //下一个元素的数组下标 5 } SLinkList[MaxSize];
顺序表和链表的比较:
1) 存取方式不同。顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。
2) 逻辑结构与物理结构:采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素其物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。
3) 查找、插入和删除操作:对于按值查找,当顺序表在无序的情况下,两者的时间复杂度均为O(n);当顺序表有序时采用折半查找,时间复杂度为O(logN)。
对于按序号查找,顺序表只吃随机访问,时间复杂度仅为O(1),而链表的平均时间复杂度为O(n)。顺序表的插入、删除操作,平均需要移动半个表厂的元素。链表的插入、删除操作,只需要修改相关结点的指针域即可。由于链表每个节点带有指针域,因而在存储空间上比顺序存储要付出较大的代价,存储密度不够大。
4) 空间分配:顺序存储在静态存储器分配情形下,一旦存储空间装满就不能扩充。动态存储分配虽然可以扩充,但需要移动大量元素,导致操作效率降低。链式存储的借点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
在实际中应怎样选取存储结构?
1) 基于存储的考虑:对线性表的长度或存储规模难以估计时,不宜采用顺序表。链表不用实现估计存储规模,但链表的存储密度较低。
2) 基于运算的考虑:若经常按序号访问数据元素,则顺序表优于链表。插入、删除操作时链表优于顺序表。(基于时间复杂度)
3) 基于环境的考虑:顺序表容易实现,链表的操作基于指针。顺序表实现较为简单。