大纲:
1、线性表-关于顺序存储的实现(增删改查)
2、线性表-关于链式(单链表)存储的设计(增删改查与头插法/尾插法)
线性表
1、顺序表
对于非空的线性表和线性结构,其特点如下:
存在唯一的一个被称作“第一个”的数据元素;
存在唯一的一个被称作“最后一个”的数据元素;
除了第一个之外,结构中的每个数据元素均有一个前驱
除了最后一个之外,结构中的每个数据元素都有一个后续。
链表结构与顺序存储结构优缺点对比
1、存储分配方式
- 顺序存储结构用一段连续的存储单元一次存储线性表的数据元素;
- 单链表采用链式存储结构,用一组任意的存储单元放线性表的元素;
2、时间性能
查找
- 顺序存储 O(1)
- 单链表 O(n)
插入和删除
- 存储结构插入需要在插入位置后面的向后移动空出插入位置,删除需要删除后后面的向前移动(平均需要移动一个表长一半的元素),时间 O(n)
- 单链表查找某位置后的指针后,插入和删除为 O(1)
3、空间性能
- 顺序存储结构需要预先分配存储空间,分太大的话浪费空间,分太小的话容易发生溢出;
- 单链表不需要预先分配存储空间,只要有就可以分配使用,元素个数也不受限制;
线性表常见设计方式
线性表 ->顺序存储(逻辑相邻,物理存储地址相邻)(a[1] 与 a[2]相邻)
ADT list{
Data:线性表的数据对象集合为{a1,a2,a,......an},每个元素的类型均为DataType,其中,除了第一个元素a1 外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an 外,每个元素有且只有一个直接后续元素,数据元素之间的关系是一对一的关系。
OPeration(操作)
InitList(&L) 初始化表
操作结果:初始化操作,建立一个空的线性表L
DestroyList(&L) 销毁表
初始条件:线性表L已存在
操作结果:销毁线性表L
ClearList(&L) 清空表
初始条件:线性表L已存在
操作结果:将L重置为空表
ListEmpty(L) 表是否为空
初始条件:线性表L已存在
操作结果:若L为空表,则返回true,否则返回false
ListLength(L) 表长度(元素个数)
初始条件:线性表L已存在
操作结果:返回L中数据元素的个数
......
GetElem(L,i,&e) 获取元素
初始条件: 线性表L已存在,且1<=i<ListLength(L)
操作结果: 用e返回L中第i个数据元素的值;
LocateElem(L,e)
初始条件: 线性表L已存在
操作结果: 返回L中第1个值与e相同的元素在L中的位置. 若数据不不存在则返回0;
PriorElem(L, cur_e,&pre_e);
初始条件: 线性表L已存在
操作结果: 若cur_e是L的数据元素,且不不是第⼀一个,则⽤用pre_e返回其前驱,否则操作失败.
NextElem(L, cur_e,&next_e);
初始条件: 线性表L已存在
操作结果: 若cur_e是L的数据元素,且不不是最后⼀一个,则⽤用next_e返回其后继,否则操作失败.
......
ListInsert(L,i,e);
初始条件: 线性表L已存在,且1<=i<=listLength(L)
操作结果: 在L中第i个位置之前插⼊入新的数据元素e,L⻓长度加1.
ListDelete(L,i);
初始条件: 线性表L已存在,且1<=i<=listLength(L)
操作结果: 删除L的第i个元素,L的⻓长度减1.
TraverseList(L);
初始条件: 线性表L已存在
操作结果: 对线性表L进⾏行行遍历,在遍历的过程中对L的每个结点访问1次.
}ADT List.
单链表
结点
结点:数据域 和 指针域
单向链表的不能通过一个结点找到前一个结点(双向链表和循环链表可以)
线性表的最大特点是不连续的,每个数据与数据通过指针域进行连接。
单链表逻辑状态
设计单链表的时候可以加一个头结点
头结点 在首元结点(单链表中第一个带有值的节点)之前。头结点的好处是不需要对头结点做特殊处理
初始化的时候就要加入头结点
头结点的主要作用是做增删改查的
函数执行完毕的,时候函数内的临时变量等都会被销毁
引申:
xcode 创建一个C语言工程
单链表操作
#include <stdio.h>
#include "string.h"
#include "ctype.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define MAXSIZE 20
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;//ElemType 类型根据 实际情况而定,这里假设为int
typedef int Status;//Status 是很熟的类型,其值是函数结果状态代码,如OK等
//设计一个结构体
typedef struct Node {
ElemType data;//数据域 不需要指针,因为这是一个指针
struct Node *next;//指针域 指向整个结构体的指针 -> Node
}Node;
//重定义一下 以免每次定义都很麻烦
typedef struct Node * LinkList;
1、初始化
//1、初始化
Status InitList(LinkList *L){
*L = (LinkList)malloc(sizeof(Node));
// 存储空间分配失败
if (*L == NULL) return ERROR;
(*L)->next = NULL;//头结点的next 指向首元结点。初始化的时候next 还是空
// 头结点不是插入进去的,是初始化的时候就带入进入的
return OK;
}
int main(int argc, const char * argv[]) {
// insert code here...
printf("Hello, World!\n");
//1、初始化
//L1 和 L2 是等价的,L2是前面重定义了
LinkList L1,L,L3;
struct Node * L2;
Status iStatus;
ElemType e;
// 初始化,但是一般不会这么初始化。这里用来演示
// L1 = (LinkList)malloc(sizeof(Node));
// L2 = (LinkList)malloc(sizeof(Node));
// L1->data = 1;
// L2->data = 2;
// 1、单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
2、链表的遍历
//2、单链表的遍历
Status ListTraverse(LinkList L){
// 遍历的时候应该从首元结点开始,L现在是指向头结点,所以p = L->next;
LinkList p = L->next;
while (p) {
printf("%d ",p->data);
p=p->next;
}
printf("\n");
return OK;
}
ListTraverse(L);
3、单链表插入
思路:
例如在 a b 两个结点中间插入 c
1、查找需要插入的前面一个结点 a
2、创建一个新的结点,让S 指向这个新的结点 c
3、给新创建的结点赋值 c
4、将新创建的结点 指针域指向插入位置的结点 c->b
5、将插入前一个结点指向新创建的结点 a->c
必须先4 再5,因为如果先把 a指向c,那么b就会丢失在链表中,所以必须先把c 指向b,再去把a 指向c。
//3、单链表插入
//首先写一个循环
Status ListInsert(LinkList *L,int i,ElemType e){
int j;//用来计数的
LinkList p,s;//p 用来遍历,s 创建新结点
p = *L;//p 指向*L
j = 1;//计数,0位置是头结点,从1开始。
// p为空 就没必要向下找了,同时
while (p && j<i) {
p = p->next;//找到前一个结点()
++j;
}
if (!p || j>i) return ERROR;
// 1、创建新的结点
s =(LinkList)malloc(sizeof(Node));
s->data = e;
// 2、插入,将前面的结点指向显得结点
s->next = p->next;
p->next = s;
return OK;
}
// 2、单链表插入 数据
for(int j = 1;j<=10;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
//L 插入后
//10 9 8 7 6 5 4 3 2 1
4、单链表删除
例如在 a b c 三个结点删除中间 b结点
1、找到待删除前一个结点 a
2、q 指向待删除的结点 q->b
3、将前面结点的next 指向 删除后面的结点 a->c
4、将需要删除的结点 free
必须要用q 指向b,因为修改后如果不指向就找不到b 了。因为b 必须要被free 将内存空间空出来给内存池,交给别人用
//4、单链表删除
//初始条件:顺序线性表L 已存在,1<=ListLength(L)
//操作结果:删除L 的第i 个数据元素,并用e 返回其值,L的长度减1
Status ListDelete(LinkList *L,int i,ElemType *e){
int j;
LinkList p,q;
p = (*L)->next;
j =1;
// 查找第i-1 个结点,p指向该结点
while (p->next && j<(i-1)) {
p = p->next;
++j;
}
// 当i>n 或者 i<1 时,删除位置不合理
if (!(p->next) || j>i-1) return ERROR;
// q指向要删除的结点
q = p->next;
// 将q 的后续赋值给p 的后续
p->next = q->next;
// 将q结点中的数据给e
*e = q->data;
// 让系统回收此结点,释放内存;
free(q);
return OK;
}
// 3、删除第5个元素
iStatus = ListDelete(&L, 5, &e);
printf("删除第5个元素为: %d\n",e);
ListTraverse(L);
//删除第5个元素为: 6
//10 9 8 7 5 4 3 2 1
5、头插法(前插法)
每次都从线性表的首元结点之前插入数据。
//5、头插法(前插法)
void CreateListHead(LinkList *L,int n){
LinkList p;
// 建立一个带头结点单链表
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
for (int i=0; i<n; i++) {
p=(LinkList)malloc(sizeof(Node));
p->data = i;//i 是演示数据
p->next = (*L)->next;//将插入的结点指针指向原来的首元结点(也就是头结点的指向 *L->next)
(*L)->next = p;//将 *L指针指向新的结点 p
}
}
// 4、头插法
CreateListHead(&L1, 20);
printf("头插法创建的L1的元素:\n");
ListTraverse(L1);
//头插法创建的L1的元素:
//19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
6、尾插法(后插法)
每次都从尾结点插入结点
//6、尾插法(后插法)
//创建两个指针,p指向新创建需要插入的结点 r指向尾结点
void CreataListTail(LinkList *L,int n){
LinkList p,r;
// 建立一个带头结点单链表
*L = (LinkList)malloc(sizeof(Node));
r = *L;
for (int i=0; i<n; i++) {
p = (LinkList)malloc(sizeof(Node));
p->data = i;//演示数据
r->next = p;//
r = p;
}
r->next = NULL;
}
// 4、尾插法
CreataListTail(&L1, 20);
printf("尾插法创建的L1的元素:\n");
ListTraverse(L1);
//尾插法创建的L1的元素:
//0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19