第二章 线性表
2.1 线性表及其逻辑结构
2.1.1 线性表的定义
线性表:由n(n≥0,n=0时称为空表)个具有相同特性的数据元素构成的一个有限序列。
[图2.1 线性表逻辑结构的图形表]:
线性表的特性:
- 有穷性:一个线性表中的元素个数是有限的。
- 一致性:一个线性表中的所有元素的性质相同。(所有元素具有相同的数据类型)
- 序列性:一个线性表中所有元素之间的相对位置是线性的,即存在唯一的开始元素和终端元素,除此之外,每个元素只有唯一的前驱元素和后继元素。各个元素在线性表中的位置只取决于它们的序号,所以在一个线性表中可以存在两个值相同的元素。
2.1.2 线性表的抽象数据类型描述
ADT List
{
数据对象: D D D={ a i ∣ 1 ≤ i ≤ n , n ≥ 0 , a i a_i|1\leq i\leq n,n\geq 0,a_i ai∣1≤i≤n,n≥0,ai 为ElemType类型} //ElemType是自定义类型标识符
数据关系:
R R R={< a i a_{i} ai, a i + 1 a_{i+1} ai+1>| a i a_i ai、 a i + 1 ∈ a_{i+1}\in ai+1∈D, i i i=1, … , n − 1 n-1 n−1}
基本运算:
InitList( & L \&L &L):初始化线性表,构造一个空的线性表 L L L 。
DestroyList( & L \&L &L):销毁线性表,释放线性表 L L L 占用的内存空间。
ListEmpty( L L L):判断线性表是否为空表,若 L L L 为空表,则返回真,否则返回假。
ListLength( L L L):求线性代数的长度,返回 L L L 中元素的个数。
DispList( L L L):输出线性表,当线性表 L L L 不为空时顺序显示 L L L中各节点的值域。
GetElem( L , i , & e L,i,\&e L,i,&e):求线性表中某个元素值,用 e e e 返回L中第 i i i ( 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n) 个元素的值。
LocateElem( L , e L,e L,e):按元素值查找,返回 L L L 中第一个值域与 e e e 相等的元素的序号,若这样的元素不存在,则返回值为0。
ListInsert( & L , i , e \&L,i,e &L,i,e):插入数据元素,在 L L L 的第 i ( 1 ≤ i ≤ n ) i (1 \leq i \leq n) i(1≤i≤n) 个位置插入一个新的元素 e , L e,L e,L 的长度增1。
ListDelete( & L , i , & e \&L,i,\&e &L,i,&e):删除数据元素,删除 L L L 的第 i ( 1 ≤ i ≤ n ) i (1 \leq i \leq n) i(1≤i≤n) 个元素,并用 e e e 返回其值, L L L 的长度减1。
}
【说明】
- 某些数据结构上的基本预算,不是它的全部运算,而是一些常用的基本运算,而每一个基本运算在实现时也可能根据不同的存储结构派生出一系列相关的运算来。
- 上述线性表 L L L 仅仅是一个抽象在逻辑层次的线性表,尚未涉及存储结构,因此每个操作在逻辑结构层次上尚不能用具体的某种程序语言写出具体的算法,而算法的实现只有在存储结构确立之后。
2.2 线性表的顺序存储结构
2.2.1 线性表的顺序存储结构——顺序表
把线性表中的所有元素按照其逻辑结构依次存储到计算机中的一块连续的存储单元。
线性表 <----> 逻辑结构
顺序表 <----> 存储结构
元素地址计算方法:
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 ) = L O C ( a 1 ) + ( i − 1 ) ∗ L LOC(a_i)=LOC(a_1)+(i-1)*L LOC(ai)=LOC(a1)+(i−1)∗L
其中:
L L L:一个元素所占的存储空间大小(字节数)
L O C ( a i ) LOC(a_i) LOC(ai):线性表第 i i i 个元素的地址
顺序表的特点:
逻辑上相邻——物理地址相邻
随机存取
2.2.2 顺序表基本运算的实现(C语言描述)
-
建立顺序表
#include <stdio.h> #include <stdlib.h> #define MAXSIZE 20 typedef struct { int data[MAXSIZE]; //当前元素 int length; //当前长度 }SqList; //建立顺序表 void CreateList(SqList *L, int a[], int n) //由a中的n个元素建立顺序表 { int i = 0, k = 0; //k表示L中的元素个数,初始值为0 L = (SqList *)malloc(sizeof(SqList)); //分配存放线性表的空间 while (i<n) //i扫描数组a的元素 { L->data[k] = a[i]; //将元素a[i]存放在L中 k++; i++; } L->length = k; //设置L的长度 }
-
顺序表的基本运算算法
- 初始化可行性表 InitList(&L)
void InitList(SqList *L) { L= (SqList *)malloc(sizeof(SqList)); //分配存放线性表的空间 L->length = 0; //置空线性表长度为0 }
时间复杂度为 O ( 1 ) O(1) O(1)
- 销毁线性表DestroyList(&L)
void DestroyList(SqList *L) { free(L); //释放L所指的顺序表空间 }
时间复杂度为 O ( 1 ) O(1) O(1)
- 判断线性表是否为空表ListEmpty(&L)
bool ListEmpty(SqList *L) { return(L->length == 0); }
时间复杂度为 O ( 1 ) O(1) O(1)
- 求线性表的长度ListLength(&L)
int ListLength(SqList *L) { return(L->length); }
时间复杂度为 O ( 1 ) O(1) O(1)
- 输出线性表DispList(&L)
void DispList(SqList *L) { for (int i = 0; i < L->length; i++) //扫描顺序表输出各元素值 { printf("%d ", L->data[i]); } }
时间复杂度为 O ( n ) O(n) O(n)
- 求线性表中的某个数据元素值GetElem(L, i, &e)
bool GetElem(SqList *L, int i, int &e) { if (i<i || i>L->length) return false; //参数i错误时返回false e = L->data[i - 1]; //取元素值 return true; //成功找到元素时返回true }
时间复杂度为 O ( 1 ) O(1) O(1)
- 按元素值查找LocateElem(&L, e)
int LocateElem(SqList *L, int e) { int i = 0; while (i < L->length&&L->data[i = 1] != e) i++; //查找元素e if (i >= L->length) //未找到时返回0 return 0; else { return i + 1; //找到后返回其逻辑序号 } }
时间复杂度为 O ( n ) O(n) O(n)
- 插入数据元素ListInsert(&L, i, e)
bool ListInsert(SqList *L, int i, int e) { if (i<1 || i>L->length + 1) return false; //参数i错误时返回false i--; //将顺序表逻辑序号转换为物理序号 for (int j = L->length; j > i; j++) //将data[i]及后面的元素后移一个位置 L->data[j] = L->data[j - 1]; L->data[i] = e; //插入元素e L->length++; //顺序表长度增1 return true; //成功插入返回true }
时间复杂度为 O ( n ) O(n) O(n)
- 删除数据元素ListDelete(&L, i, &e)
bool ListDelete(SqList *L, int i, int &e) { if (i<1 || i>L->length) //参数i错误时返回false return false; i--; //将顺序表逻辑序号转换为物理序号 e = L->data[i]; for (int j = i; j < L->length - 1; j--) //将data[i]之后的元素前移一个位置 L->data[j] = L->data[j + 1]; L->length--; //顺序表长度减1 return true; //成功插入返回true }
时间复杂度为 O ( n ) O(n) O(n)
小结
线性表顺序存储结构的特点:逻辑关系上相邻的两个元素在物理存储位置上也相邻。
优点:可以随机存取表中任一元素 O ( 1 ) O(1) O(1) ;存储空间使用紧凑。
缺点:在插入、删除某一元素时,需要移动大量元素 O ( n ) O(n) O(n) ;预先分配空间需按最大空间分配,利用不充分。
2.3 线性表的链式存储结构
2.3.1 线性表的链式存储结构——链表
-
链表概述
结点
数据域:存储数据元素本身信息。
指针域:指示直接后继(前驱)的存储位置,指针域中存储的信息称作指针或链。
N个结点的存储映像链结成一个链表,即为线性表的链式存储结构,即链表。
每个结点中除包含有数据域以外值设置一个指针域,用于指向其后继结点,这样构成的链表称为线性单向链表,简称单链表。
每个结点中除包含有数据域以外值设置两个指针域,分别用于指向其前驱结点和后继结点,这样构成的链表称为线性双向链表,简称双链表。
头指针:线性表的链式存储结构中,通常每个链表带有一个头结点,并通过头结点的指针唯一标识该链表,称为头指针。
头结点:在单链表的第一个结点之前附设一个结点,它没有直接前驱,称之为头结点。
首结点:链表中第一个结点称为首结点(或开始节点)。
首指针:相应的指向首结点或者开始结点的指针称为首指针。
尾结点:链表中最后一个结点称为尾结点。
尾指针:指向尾结点的指针称为尾指针。
-
链表和顺序表的比较
在顺序表中,逻辑上相邻的元素对应的存储位置也相邻,所以当进行插入或删除操作时通常需要平均移动半个表的元素。
在链表中,逻辑上相邻的元素对应的存储位置是通过指针来链接的,因而每个结点的存储位置可以任意安排,不必要求相邻,所以当进行插入或删除操作时只需要修改相关结点的指针域即可。
顺序表的存储密度比较高。
存储密度:结点中数据元素本身所占的存储量和整个结点占用的存储量之比,即
存 储 密 度 = 结 点 中 数 据 元 素 所 占 的 存 储 量 结 点 所 占 的 存 储 量 存储密度=\frac{结点中数据元素所占的存储量}{结点所占的存储量} 存储密度=结点所占的存储量结点中数据元素所占的存储量
2.3.2 单链表
定义:每个结点中只含一个指针域的链表,叫单链表。
LinkNode类型声明:
typedef struct LNode
{
int data; //存放元素值
struct LNode *next; //指向后继结点
}LinkNode; //单链表结点类型
在后面算法设计中,如果没有特别说明,均采用带头结点的单链表,在单链表中增加一个头结点的优点如下:
- 单链表中首结点的插入和删除操作与其他结点一致,无须进行特殊处理。
- 无论单链表是否为空都有一个头结点,因此统一了空表和非空的处理过程。
空表的表示
无头结点时,当头结点的值为空时表示空表。
有头结点时,当头结点的指针域为空时表示空表。
-
插入和删除结点的操作
- 插入节点的操作
s->next=p->next; p->next=s;
- 删除结点的操作
q=p->next; //q临时保存被删除的结点 p->next=q->next; //从链表中删除结点q free(q); //释放结点q的空间
-
建立单链表
- 头插法
s->next=L->next; L->next=s;
- 尾插法
r->next=s; r=s;
-
线性表基本运算在单链表中的实现
- 初始化线性表InitList(&L)
void InitList(LinkNode *L) { L = (LinkNode *)malloc(sizeof(LinkNode)); L->next = NULL; //创建头结点,其next域置NULL }
时间复杂度为 O ( 1 ) O(1) O(1)
- 销毁线性表DestroyList(&L)
void DestroyLsit(LinkNode *L) { LinkNode *pre = L, *p = L->next; //pre指向结点p的前驱结点 while (p != NULL) //扫描单链表L { free(pre); //释放pre结点 pre = p; //pre、p同步后移一个结点 p = pre->next; } free(pre); //循环结束时p为NULL,pre指向尾结点,释放它 }
时间复杂度为 O ( n ) O(n) O(n)
- 判断线性表是否为空表ListEmpty(&L)
bool ListEmpty(LinkNode *L) { return(L->next == NULL); }
时间复杂度为 O ( 1 ) O(1) O(1)
- 求线性表的长度ListLength(&L)
int ListLength(LinkNode *L) { int n = 0; LinkNode *p = L; //p指向头结点,n置为0(即头结点的序号为0) while (p->next!=NULL) { n++; p = p->next; } return(n); //循环结束,p指向尾结点,其序号n为结点个数 }
时间复杂度为 O ( n ) O(n) O(n)
- 输出线性表DispList(&L)
void DispList(LinkNode *L) { LinkNode *p = L->next; //p指向首结点 while (p!=NULL) //p不为NULL,输出p结点的data域 { printf("%d", p->data); p = p->next; //p移向下一个结点 } printf("\n"); }
时间复杂度为 O ( n ) O(n) O(n)
- 求线性表中的某个数据元素值GetElem(L, i, &e)
bool GetElem(LinkNode *L, int i, int &e) { int j = 0; LinkNode *p = L; //p指向头结点,j置为0(即头结点的序号为0) if (i <= 0)return false; //i错误返回假 while (j<i&&p!=NULL) //找第i个结点p { j++; p = p->next; } if (p == NULL) //不存在第i个数据结点,返回false return false; else //存在第i个数据结点,返回true { e = p->data; return true; } }
时间复杂度为 O ( n ) O(n) O(n)
- 按元素值查找LocateElem(&L, e)
int LocateElem(LinkNode *L, int e) { int i = 1; LinkNode *p = L->next; //p指向首结点,i置为1(即首结点的序号为1) while (p!=NULL&&p->data!=e) //查找data值为e的结点,其序号为i { p = p->next; i++; } if (p == NULL) //不存在值为e的结点,返回0 return 0; else //存在值为e的结点,返回其逻辑序号i return(i); }
时间复杂度为 O ( n ) O(n) O(n)
- 插入数据元素ListInsert(&L, i, e)
bool ListInsert(LinkNode *L, int i, int e) { int j = 0; LinkNode *p = L, *s; //p指向头结点,j置为0(即头结点的序号为0) if (i <= 0) return false; //i错误返回false while (j<i-1&&p!=NULL) //查找第i-1个结点p { j++; p = p->next; } if (p == NULL) //未找到第i-1个结点,返回false return false; else //找到第i-1个结点p,插入新结点并返回true { s = (LinkNode *)malloc(sizeof(LinkNode)); s->data = e; //创建新结点s,其data域为e s->next = p->next; //将结点s插入到结点p之后 p->next = s; return true; } }
时间复杂度为 O ( n ) O(n) O(n)
- 删除数据元素ListDelete(&L, i, &e)
bool ListDelete(LinkNode *L, int i,int &e) { int j = 0; LinkNode *p = L, *q; //p指向头结点,j置为0(即头结点的序号为0) if (i <= 0) return false; //i错误返回false while (j<i-1&&p!=NULL) //查找第i-1个结点p { j++; p = p->next; } if (p == NULL) //未找到第i-1个结点,返回false return false; else //找到第i-1个结点p { q = p->next; //q指向第i个结点 if (q == NULL) //若不存在第i个结点,返回false return false; e = q->data; p->next = q->next; //从单链表中删除q结点 free(q); //释放q结点 return true; //返回true表示成功删除第i个结点 } }
时间复杂度为 O ( n ) O(n) O(n)
2.3.3 双链表
typedef struct DNode
{
int data; //存放数据元素
struct DNode *prior; //指向前驱元素
struct DNode *next; //指向后继元素
}DLinkNode; //双链表的结点类型
-
建立双链表
-
头插法
-
尾插法
-
-
线性表基本运算在双链表中的实现
- 插入
s->next=p->next; p->next->prior=s; s->prior=p; p->next=s;
- 删除
p->next=q->next; q->next->prior=p; free(q);
- 插入
2.3.4 循环链表
循环链表是另一种形式的链式存储结构。有循环单链表和循环双链表两种类型。
尾结点->next=NULL ==> 尾结点->next=头结点
尾结点->next=NULL ==> 尾结点->next=头结点
头结点->prior=NULL ==> 头结点->prior=尾结点
2.4 线性表的运用
2.5 有序表
本章小结
- 理解线性表的逻辑结构特性
- 掌握线性表的两种存储方式,即顺序表和链表,体会这两种存储结构之间的差异。
- 掌握顺序表上各种基本运算的实现过程和顺序表的通用算法设计方法。
- 掌握单链表上各种基本运算的实现过程和单链表的通用算法设计方法。
- 掌握双链表的特点和双链表的通用算法设计方法。
- 掌握循环双链表的特点和对应非循环链表的差别。
- 掌握有序的特点和二路归并算法,以及利用有序表设计高效的算法。
- 综合运用线性表解决一些复杂的实际问题。