线性表
线性结构
- 定义及特点: 若结构是非空有限集,有且仅有一个开始结点和一个终端结点,并且所有结点都最多只有一个直接前驱和一个直接后继。
- 线性结构包括线性表、栈、队列、字符串、数组等等。其中,最典型、最常用的是线性表。
- 同一线性表中的元素必定具有相同特性
一、 线性表的类型定义
ADT List{
数据对象:D={ ai | ai ∈ElemSet, i=1,2,…,n, n≥0 }
数据关系:R1={ <ai-1 ,ai >|ai-1 ,ai∈D, i=2,…,n }
基本操作:
- {结构初始化}
InitList( &L )
操作结果:构造一个空的线性表 L。- {结构销毁}
DestroyList( &L )
初始条件:线性表 L 已存在。
操作结果:销毁线性表 L。- 引用
ListEmpty( L )
ListLength( L )
PriorElem( L, cur_e, &pre_e )
NextElem( L, cur_e, &next_e )
GetElem( L, i, &e )
LocateElem( L, e, compare( ) )
ListTraverse(L, visit( ))- 加工
ClearList( &L )
( 线性表置空 )
PutElem( &L, i, e )
( 改变数据元素的值 )
ListInsert( &L, i, e )
( 插入数据元素 )
ListDelete( &L, i, &e )
( 删除数据元素 )
}ADT List
- 线性表的基本操作根据实际应用而定;
- 复杂的操作可以通过基本操作的组合来实现;
二、 顺序表示和实现
线性表的顺序表示又称为顺序存储结构或顺序映像。
2.1 顺序表的表示
- 顺序存储定义: 用逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。
- 顺序存储方法: 用一组地址连续的存储单元依次存储线性表的元素,可通过数组V[n]来实现。
- 存储特点
- 逻辑相邻,则物理相邻
- 利用数组下标,从首元素地址可推出其他元素存放地址
- 优点:随机存取,查找快 O(1)
- 缺点:插入、删除慢 O(n)
存取结构和存储结构
存储结构和存取结构是两种不同的概念:
- 存储结构是数据及其逻辑结构在计算机中的表示。
- 存取结构是在某种数据结构上对查找操作时间性能的描述。如:随机存取,顺序存取
2.2 顺序表的实现
- 定义与初始化
#define LIST_INIT_SIZE 100 //存储空间初始分配量 #define LISTINCREMENT 10 //存储空间分配增量 struct SqList { // typedef 给结构类型起别名 int *elem; //表基址 int length; //表长(特指元素个数) int listsize; //表当前存储容量 }; //初始化顺序表 void function1(SqList *array) { array->elem = (int *)malloc(sizeof(int)*LIST_INIT_SIZE); if (!array->elem) { cout << "内存分配失败!已退出!"; exit(1); } array->length = 0; // 空表长度为0 array->listsize = LIST_INIT_SIZE; // 初始存储容量 };
- 删除
// 删除线性表第x个位置上的元素 void function11(SqList *array,int x){ if (x<1||x>array->length) { cout<<"输入位置不合法,已退出!"; exit(1); }else { for (int *p = &array->elem[x-1]; p < &array->elem[array->length-1] ; p++) { *p = *(p+1); } array->length-=1; } }
- 插入
// 10.在线性表指定位置插入元素y void function10(SqList *array,int x,int y){ if (x<1||x>array->length+1) { cout<<"位置不合法!\n"; } else { if (array->length >= array->listsize) { int *newbase = (int *)realloc(array->elem, sizeof(int) * (LISTINCREMENT + array->listsize)); if (!newbase) { cout << "重分配空间失败!\n"; } else { array->elem = newbase; array->listsize += LISTINCREMENT; int *q = &array->elem[x - 1]; for (int *p = &array->elem[array->length]; p > q; p--) { *p = *(p-1); } *q = y; array->length += 1; } } else { int *q = &array->elem[x - 1]; for (int *p = &array->elem[array->length]; p > q; p--) { *p = *(p - 1); } *q = y; array->length += 1; } } }
- 销毁线性表
//销毁顺序表 void function2(SqList *array){ free(array->elem); array->elem = NULL; array->length=0; array->listsize=0; }
2.3 顺序表的运算效率分析
-
插入操作的时间效率
若在长度为 n 的线性表的第 i 位前 插入一元素,则向后移动元素的次数f(n)为:
f ( n ) = n − i + 1 f(n) =n-i+1 f(n)=n−i+1 -
删除的时间效率
若在长度为 n 的线性表上删除第 i 位元素,向前移动元素的次数f(n)为:
f ( n ) = n − i f(n) =n-i f(n)=n−i -
空间效率
顺序表的空间复杂度S(n)=O(1)
三、 链式表示和实现
3.1 链表的表示
- 链式存储特点:逻辑上相邻,物理上不一定相邻
- 链式存储术语:
- 单链表:结点只有一个指针域的链表,或称为线性链表;
- 双链表:有两个指针域的链表;
- 循环链表:首尾相接的链表。
- 首元节点:指链表中存储线性表第一个数据元素a1的结点。
- 头结点:在链表的首元结点之前附设的一个结点;数据域内只放空表标志和表长等信息(不一定)
表示空表:
- 无头结点时,当头指针的值为空时表示空表
- 有头结点时,当头结点的指针域为空时表示空表
设置头结点的好处
- 带头结点的链表任何元素节点都有前驱结点,若链表没有头结点,则首元素结点没有前驱结点,在其前插入结点或删除该结点时操作会复杂些。
- 对带头结点的链表,表头指针是指向头结点的非空指针,因此空表与非空表的处理是一样的。这样可以区分空链表和空指针,也减少了程序的复杂性和出现bug的机会。
头结点的数据域:可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
3.2 链表的实现(全为头结点示例)
struct LinkNode
{
int num;
LinkNode *next;
};
- 单链表的建立
- 头插法
//建立链表头插法 LinkNode* create(int n){ LinkNode *head; //头结点 head = (LinkNode*)malloc(sizeof(LinkNode)); head->next = null; for (int i = n; i >0; i--){ LinkNode *q = (LinkNode *)malloc(sizeof(LinkNode)); if ( q == NULL){ cout << "alarm!"; exit(1); } cout<<"请输入该节点的整数值:"; cin >> q->num; q->next = head->next; head->next = q; } return head; }
- 尾插法
//建立链表尾插法 LinkNode* create(int n){ LinkNode *head, *p; head = (LinkNode *)malloc(sizeof(LinkNode)); head->next = null; p = head; for (int i = 0; i < n; i++) { LinkNode *q = (LinkNode *)malloc(sizeof(LinkNode)); if (q == NULL) { cout << "alarm!"; exit(1); } cout << "请输入该节点的整数值:"; cin >> q->num; p->next = q; p = q; } return head; }
- 头插法
- 单链表的读取(修改)
//获取第n个元素节点数据(有头结点) void elementGet(LinkNode *L, int n){ int i = 0; LinkNode *p = L; while (p && i<n) { p = p->next; i++; } if (!p || i > n) exit(1); cout << p->num << " "; }
- 单链表的插入
//第n个节点后插入新节点 LinkNode* insert(LinkNode *L, int n){ int i = 0; LinkNode *p = L; while (p && i < n) { p = p->next; i++; } LinkNode *q = (LinkNode*)malloc(sizeof(LinkNode)); if (q == NULL) { cout << "alarm!"; exit(1); } cout << "请输入新节点数据值:"; cin>>q->num; q->next = p->next; p->next = q; return L; }
- 删除指定位置元素
void Delete(LinkNode *L, int n){ int i = 0; LinkNode* p = L; while (p && i < n-1) { p = p->next; i++; } LinkNode *q = p->next; p->next = q->next; free(q); }
3.3 链表的运算效率分析
-
查找
因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。 -
插入和删除
因线性链表不需要移动元素,在给出某个合适位置的指针后,插入和删除操作所需的时间仅为 O(1);如果未给出合适位置的指针,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。书中的两个算法的时间复杂度都为O(n)。 -
空间效率分析
链表中每个结点都要增加一个指针空间,相当于总共增加了n 个整型变量,空间复杂度为 O(n)。
总结
顺序存储 | 链式存储 | |
---|---|---|
存储密度 | 1 | <1 |
分配方式 | 静态分配,会出现空间闲置和溢出 | 动态分配,不会出现空间闲置或溢出 |
存取方法 | 随机存取,时间复杂度O(1) | 顺序存取,链表中的结点需从头指针起顺着链扫描才能取得,时间复杂度O(n) |
插入删除操作 | 在顺序表中进行插入和删除操作,平均要移动表中一半的结点,时间复杂度为O(n)。 | 不需要移动元素,确定插入、删除位置后,时间复杂度为O(1)。 |
- 事实上,链表插入、删除运算的快慢是以空间代价来换取时间。
- 存储密度=结点数据本身所占的存储量/结点结构所占的存储总量
- 顺序表适宜于做查找这样的静态操作;链表宜于做插入、删除这样的动态操作。
- 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
- 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。