线性表的定义 :
简而言之 : 0 个 或 多个元素(类型相同)的有限序列( 有顺序 ) , 第一个元素无前驱 , 最后一个元素无后继 , 其他元素 与有唯一的前驱 和 唯一的后继
数学语言定义 : 若将线性表记为 ( a1 , a2 , ..... , ai - 1 , ai , ai+1 , ... , an) , 则表中 ai-1 领先于 ai , ai 领先于 ai+1
称 ai-1 是 ai 的前驱 , ai+1 是 ai 的后继元素 . 当 i = 1 , 2 ... , n - 1 时候 , ai 有且仅有 一个 直接后
继 , 当 i = 2 ,......., n 时候 , ai 有且仅有一个直接的前驱。
线性表的长度 : 线性表的元素的个数 n( n>=0 ) , 当 n = 0 时候 我们称之为空表
线性表的ADT :
注意 : 这只是 抽象 层面 来说 , 也就是说实现方式尽管 有 顺序表 和 连表等实现方式 , ADT仅仅抽取他们的相同
处 且 基本具有的操作 , 具体实现时候 , 可以添加Data 数据 , 可以 扩充 操作 或者添加 辅助方法( 抽取
相同代码 )。 对于实际问题中更复杂的操作 , 可以使用ADT中的基本操作来组合完成。
定义如下:
ADT 线形表 ( List ) Data : 下一层数据类型 + 描述 线性表的数据对象的序列 {a1,a2,a3,......, an} , 每个元素均为DataType , 其中 , 除了元素 a1 外 , 每一个 元素都只有唯一的前驱元素 , 除了an外 , 每个元素都只有唯一的后继元素 。 元素之间的关系是一对一的关 系。 Operation: 操作 + 描述 (规定 : 提前说明 , 我们使用的编号 均从 0 开始 , 操作返回 0 代表成功 , -1 代表失败) InitList(*L) : 初始化操作 , 给L 指针指向的List类型开辟能容纳序列的空间 ListEmpty(L) : 若线性表为空 , 返回true else false ClearList(*L) : 将线性能表清空 GetElem(L , i ,*e) : 将线性表L中的第i个元素通过传出参数e取出 , 返回值 0 代表的 成功 , -1 代表错误 LocateElem(L,e ) : 将线形表L 中查找 第一个匹配到的e , 成功返回 编号位置 , 失败返回-1 ListInsert(*L , i , e) : 在线性表第 i 个元素 的前面 插入一个元素 , 成功返回 0 失败返回 -1 ListDelete(*L , i ,*e ): 删除线性表中的第i个位置的元素 , 并用e返回其值 ListLength(L) : 返回线性表中的元素的个数 : endADT
线形表的顺序存储结构:
ADT -> 物理层面 -> 语言实现( C 语言实现):
指的是使用一段连续的存储单元依次存储线性表的数据元素。
sqList 结构体 定义在头文件当中:
#ifndef __SEQ_LIST_H #define __SEQ_LIST_H #define MAXSIZE 20 /*假设顺序表的容量为20*/ #define TRUE 0 /*我们只使用TRUE 和 FALSE 两个量表示成功和失败就行了 , 但是注意TRUE是0*/ #define FALSE -1 typedef int ElemType; /*假设ElemType 类型为int*/ typedef int BOOL; /*BOOL 表示两个量 : TRUE | FALSE */ /*结构体的声明放在*/ typedef struct { ElemType data[MAXSIZE]; int length; /*扩充了ATD的DATA,其表示指向序列最后一个元素的下一个位置*/ }sqList; /*函数的声明放在这个位置 */ extern void InitList(sqList * L); extern BOOL ListEmpty(sqList L); extern void ClearList(sqList * L); extern int LocateElem(sqList L , ElemType e); extern int ListLength(sqList L); extern BOOL ListInsert(sqList * L , int i , ElemType e); extern BOOL ListDelete(sqList * L , int i , ElemType *e); extern int GetElem(sqList L , int i , ElemType * e); #endif
sqList 操作定义在 .c 文件当中 :
/************************************************* Function: InitList Description: 初始化顺序表的长度为 0 Output: *L : 线性表发生变动 Return: 空 TRUE | 非空FALSE (TRUE :0 FALSE:-1) *************************************************/ void InitList(sqList * L) { L->length = 0; } /************************************************* Function: LisEempty Description: 返回顺序表是否为空 , O(1) 判断 L->length 是否为0即可 Input: L : 线性表结构 Return: 空 TRUE | 非空FALSE (TRUE :0 FALSE:-1) *************************************************/ BOOL ListEmpty(sqList L) { return L.length ? FALSE : TRUE; } /************************************************* Function: ClearList Description: 将顺序表清空 O(1) 将L->length 置为0即可 Output: *L : 线性能表发生变动 Return: void *************************************************/ void ClearList(sqList * L) { L->length = 0; } /************************************************* Function: LocateElem Description: 获取第一次出现指定元素的位置 O(n) 开始遍历判断是否相等 返回下下标位置 Input: L : 顺序表结构 e : 指定的元素 Return: int 具体位置 , 没找到返回-1 *************************************************/ int LocateElem(sqList L , ElemType e) { int i = 0; for(;i < L.length ; ++i) { if(L.data[i] == e) return i; } return -1; } /************************************************* Function: ListLength Description: 获取顺序表的长度 O(1) 直接返回 L.length即可 Input: L 表结构 Return: int 顺序表的长度 *************************************************/ int ListLength(sqList L) { return L.length; } /************************************************* Function: ListInsert Description: 在顺序表的指定位置插入元素 : 时间复杂度O(n) : 时间浪费在移动上面 : 插入位置不合理 , return FALSE 如果 length + 1 > MAXSIZE return FALSE 需要将 i 到 length-1的元素向后移动一个位置 将元素插入指定位置后 , 表的长度 + 1 Input: i : i位置元素的前面进行插入 e : 被插入的元素 Output: *L: 传出参数,表示线性表会被修改 Return: 成功 TRUE | 失败 FALSE *************************************************/ BOOL ListInsert(sqList * L , int i , ElemType e) { int j = 0; if( 0>i || i > L->length || L->length + 1 > MAXSIZE) return FALSE; for(j = i+1 ; j <=L->length ; ++j) L->data[j] = L->data[j-1]; L->data[i] = e; L->length+=1; return TRUE; } /************************************************* Function: ListDelete Description: 删除指定位置的元素 : O(n) 时间也是浪费在移动数据上面 : 如果删除的位置不合适: return FALSE 取出删除的元素 将 i 位置之后的元素都向前移动一个元素: 表的长度需要减去 1 Input: i : 删除元素的位置 : 0<=i<=L->length-1 Output: L : 表会发生变动 e : 输出被删除的元素 Return: 删除成功 TRUE | 失败 FALSE *************************************************/ BOOL ListDelete(sqList * L , int i , ElemType * e) { int j = 0; if(0 == L->length || 0 > i || i >= L->length) return FALSE; *e = L->data[i]; for(j = i+1 ; j <=L->length ; ++j) { L->data[j-1] = L->data[j]; } L->length-=1; return TRUE; } /************************************************* Function: GetElem Description: 获取顺序表中指定位置的元素 , 时间复杂度O(n) , 时间浪费在遍历上面: i位置不合理 return FALSE 取得i位置的元素 Input: L : 顺序表结构 i : 获取元素的位置 , 0<= i <= L.length-1 Output: *e : 输出类型参数 , 获取 i 指向的元素 Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL GetElem(sqList L , int i , ElemType * e) { /* i 是从0 开始计算的*/ if(0 == L.length || i < 0 || i > L.length-1) return FALSE; *e = L.data[i]; return TRUE; }
宗上看出:
主要操作:
插入 , 删除 O(n)
搜寻 , 修改 O(1)
线性能表存储结构的优点和缺点:
- 优点:
- 无需为表中元素之间的逻辑关系增加额外空间(比如连表 , 需要增加一个指针域)
- 可以快速的存取表中 任意元素的位置
- 缺点:
- 插入 和 删除操作需要移动大量元素
- 当线性表的长度变化较大时 , 难以确定存储空间的容量
- 造成存储空间的碎片 [ 可能开辟的空间大小 > 顺序表的大小造成 空间的浪费 ]
线性表的链式存储结构:
为了解决链表的使用顺序结构存储的实现方式导致的插入 和 删除元素时候的效率问题 , 使
用链式来解决 , 但是链式存储结构的随机访问效率降低(不能根据第一个元素的位置直接得出指
定位置元素的地址 , 只能通过遍历来实现)
ADT-> 链式存储结构->语言层次实现( C语言 ):
带上头结点 , 就能让在链表的头位置插入元素/删除元素变得统一
Node 结构体的定义: 放置在LinkList.h 的头文件当中
#ifndef _LINK_LIST_H #define _LINK_LIST_H #define TRUE 0 #define FALSE -1 typedef int ElemType; typedef int BOOL; typedef struct Node { ElemType data; struct Node * next; }Node; typedef struct Node * LinkList; /*基于上述链表结构体的操作的声明:*/ /*ADT 中的实现*/; extern BOOL ListInit(LinkList *L); extern BOOL ListEmpty(LinkList L); extern void ClearList(LinkList *L); extern BOOL GetElem(LinkList *L, int i, ElemType * e); extern int LocateElem(LinkList *L,ElemType e); extern BOOL ListInsert(LinkList *L, int i, ElemType e); extern BOOL ListDelete(LinkList *L, int i, ElemType *e); extern int ListLength(LinkList L); /*ADT上的操作的扩展*/ #endif
单链表的操作的实现 , LinkList.c 文件:
/** * File name: LinkList.c * Author:MusaGeek Version: 1.0 Date: 2018-11-21 * Description: 单链表的操作的实现的.c文件 * Function List: // 主要函数列表,每条记录应包括函数名及功能简要说明 BOOL ListInit(LinkList *L); 初始化链表 BOOL ListEmpty(LinkList L); 判断链表是否为空 void ClearList(LinkList *L); 清空链表 BOOL GetElem(LinkList *L, int i, ElemType * e); 获取链表指定位置的元素 int LocateElem(LinkList *L,ElemType e); 定位元素的位置 BOOL ListInsert(LinkList *L, int i, ElemType e);插入元素 BOOL ListDelete(LinkList *L, int i, ElemType *e);删除元素 int ListLength(LinkList L); 获取链表的长度 */ #include <stdio.h> #include <stdlib.h> #include "LinkList.h" /************************************************* Function: ListInit Description: 初始化单链表中的数据 1.为指向链表的头指针分配头结点空间 2.将头结点的指针域设置为NULL Input: L : 指向链表头结点的指针 Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL ListInit(LinkList *L) { Node *head = (Node *)malloc(sizeof(Node)); if(!head) return FALSE; *L = head; head->next = NULL; return TRUE; } /************************************************* Function: ListInit Description: 初始化单链表中的数据 1.为指向链表的头指针分配头结点空间 2.将头结点的指针域设置为NULL Input: L : 指向链表头结点的指针 Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL ListEmpty(LinkList L) { return ( (L->next) ? FALSE : TRUE); } /************************************************* Function: ClearList Description: 因为 LinkList 中的结点均是malloc而来的 , 因此当整个链表不使用了时候 需要手动释放掉,避免消耗内存 但是保留头结点 1.创建 指针 p = (*L)->next , 指针 q; 2.循环 :当 p 不为 NULL 时候 ; q = p->next;释放 p; p = q; Input: L : 指向链表头结点的指针 Return: void *************************************************/ void ClearList(LinkList *L) { Node *p = (*L) = (*L)->next, *q = NULL; while(p) { q = p->next; free(p); p = q; } (*L)->next = NULL; /*头结点不释放 ,指针域置为NULL*/ } /************************************************* Function: GetElem Description: 获取顺序表中指定位置的元素 , 时间复杂度O(n) , 时间浪费在遍历上面: 1.声明一个结点p指向链表的第一个结点 , 初始化 j 从1开始 2.当 j < i 时 , 就遍历链表 , 让指针p不断的向后移动 , j+=1 3.若最终 NULL == p 则 返回FALSE 4.若 NULL != p 则 将data域数据传出 返回TRUE , Input: L : 指向链表头结点的指针 i : 获取元素的位置 Output: *e : 输出类型参数 , 获取 i 指向的元素 Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL GetElem(LinkList *L, int i, ElemType * e) { Node * p = (*L)->next; int j = 1; while(p && j < i) { p = p->next; j++; } *e = p->next->data; return (p ? TRUE : FALSE); } /************************************************* Function: LocateElem Description: 定位e在链表中的位置, 时间复杂度O(n), 时间浪费在遍历上面: 遍历找到第一个匹配的元素即可 Input: L: 指向链表头结点的指针的指针 e: 需要定位的元素值 Return: int 元素在链表中的位置,没有匹配到为-1 *************************************************/ int LocateElem(LinkList *L,ElemType e) { Node *p = (*L)->next; int j = 1; while(p && p->data != e) { p = p->next; j++; } return p ? j : -1; } /************************************************* Function: ListInsert Description: 在指定 i 的位置插入元素 , O(n) , 浪费的时间在遍历上面 , 但是没有移动元素造成的复制开销 1.声明一个结点 p 指向链表的头结结点 , 初始化 j 从 1 开始 2.当 j < i 遍历链表 , 让p指针不断后移 , j+=1 3.若 遍历结束 , NULL == p , 返回FALSE 4.否则 当 j == i 时候 , p 已经指向的了 第i个元素之前的元素 5.创建结点s(malloc) , s->next = p->next; p->next = s; 6.返回TRUE Input: L : 指向链表头结点的指针 i : 获取元素的位置 e : 插入的元素值 Output: Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL ListInsert(LinkList *L, int i, ElemType e) { Node *p = *L , *s = NULL; int j = 1; while(p && j<i) //如果 i 有效的话 ,那么p最终会指向第i个结点前面的那个结点 { p = p->next; j++; } if(p) { s = (struct Node *)malloc(sizeof(Node)); //给新插入的结点分配空间 s->data = e; s->next = p->next; //下面两步骤不能反了 p->next = s; return TRUE; } return FALSE; } /************************************************* Function: ListDelete Description: 删除指定的第 i 个位置的元素 , O(n) 但是没有移动元素复制元素的开销 1.定义指针p指向第一个结点 , 定义 j = 1; 2.在 j < i 且 p !=NULL 的情况下 让指针p向后移动 , j++ 3.当 j >= i 时 若 NULL = p 那么 返回FALSE 4.否则 临时指针变量 t 保存 p->next;p->next = p->next->next; 再释放t 5.返回TRUE Input: L : 链表头指针 i : 获取元素的位置 Output: *e : 输出类型参数 , 获取 i 指向的元素 Return: 成功TRUE | 失败 FALSE *************************************************/ BOOL ListDelete(LinkList *L, int i, ElemType *e) { Node *p = *L, *t = NULL; int j = 1; while(p && j < i) //如果 i 有效的话 ,那么p最终会指向第i个结点前面的那个结点 { p = p->next; j++; } if(p) { t = p->next; p->next = t->next; //将要删除结点剔除链表 *e = t->data; free(t); return TRUE; } return FALSE; } /************************************************* Function: ListLength Description: 获取链表的长度 直接遍历链表获得 Input: L : 链表头指针 Return: int 链表的长度 *************************************************/ int ListLength(LinkList L) { Node *p = L->next; int len = 0; while(p) { len++; p = p->next; } return len; }
单链表虽然在 插入 , 删除元素上面的操作也是O(n) , 但是只是时间花费在了遍历的开销上面了 , 而不是元素移动上 , 且如果元素的类型越复杂 , 移动上面造成复制的开销将更大 , 因此单链表 比 顺序表更适合频繁的元素插入的和删除。
单链表的创建又分为 头插法 和 尾插法 两种创建方式
顺序表 和 链表的对比 :
-
- 频繁查找 , 很少进行删除 和 插入时 适用顺序表
- 线性表的元素变化较大 , 频繁的进行插入和删除 ,使用链表
- 线性表不知道元素的个数的多少的时候 , 就使用链表
静态链表:
对于早期没有指针的编程语言来说, 实现单链表可以使用静态链表方式来实现线性链表;
使用定长的数组来模仿一个链表 , 且数组中的每个元素 包含 一个 data 和 一个指向其他元素的数组下标的cur , 这样就能实现在一个连续存储空间上面的链表了。
细节注意:
将整个数组分为两个部分 : 备用链表 和 已经使用的链表 。 第一个元素的cur指向的是备用 链表的开始位置。最后一个元素的cur指向的是 已经使用的链表的第一个元素下标 [为 空时候指向的是 0 ]
静态链表的优缺点:
-
- 优点:
- 在插入 和 删除操作的时候只需要修改游标 , 不需要移动元素。
- 缺点:
- 表长度不能确定。
- 虽然物理内存空间连续 , 但是失去了像顺序表那样的随机访问的能力。
- 优点:
循环链表
将单链表的中的尾结点的指针域指向头结点即可 , 头尾相接的链表的即为循环链表。
解决问题 :
从其中某个结点出发依然能够遍历整个链表。
改进方式 :
将指向链表的指针指向循环的链表的尾巴元素 , 这样 不光 访问最后一个元素可以由O(n) 优化到O(1) , 并且对于两个链表的合并 也可以省去遍历到最后一个元素的操作 ,由O(n) 优化到O(1);
双向链表:
一个的Node中包含两个指针域 , 一个指向前驱元素(prior) , 一个指向后继元素即可(next)。初始化时候的头结点的 prior 指向自己 , next 也指向自己。
注意 : 删除元素 和 插入元素时候 涉及元素的 指针值修改的顺序需要特别的注意。
双向链表 的 插入 和 删除 :
总结:
- 线性表 : 0个或多个具有相同的类型的数据元素构成有限序列
- 顺序结构 :
- 顺序表 : 适合频繁随机访问 , 不适合频繁插入和删除
- 静态链表 : 使用顺序结构模仿 链表 : 失去了随机访问能力 , 且空间不能自动增长 , 但是改进了顺序表的移动开销
- 链式结构 :
- 单链表 : 适合 频繁 插入 和 删除 , 不适合 频繁的随机访问
- 循环链表 : 能从任意结点开始遍历整个链表 , 尾巴针的方式 改进了访问直接访问最后一个元素的时间复杂度 , 同时也改进了访问两个链表合并的时间复杂度要特别的注意。
- 双向链表 : 多增加了一个指向 前驱元素的指针域 , 插入/删除操作的顺序需要注意