填鸭字典式学习笔记之线性表

(序言)基本操作

InitList(&L) 构造一个空的线性表L,分配内存空间
DestoryList(&L) 销毁线性表,释放L所占的内存空间
ListInsert(&L,i,e) 在表L中的第i个位置上插入指定元素e
ListDelete(&L,i,&e) 删除表L中第i个位置上的元素,并用e返回删除元素的值
LocateElem(L,e) 在表L中查找具有给定关键字值的元素
GetElem(L,i) 获取表L中第i个位置的元素的值
其他常用操作
Length(L) 求表长
PrintList(L) 按前后顺序输出
Empty(L) 判空操作,为空返回true,否则为false

顺序表

实现方式

静态分配

#include<stdio.h>
#define MaxSize 10		//定义最大长度
typedef struct{
	int data[MaxSize];//用静态的“数组”存储数据元素
	int length;		//顺序表的当前长度
}SqList;			//顺序表的类型定义

//基本操作——初始化一个顺序表
void InitList(SqList &L){
	for(int i = 0; i < MaxSize; i++)
	L.data[i] = 0;		//将所有数据元素设置为默认初始值
L.length = 0;		//顺序表初始长度为0
}

int main(){
	SqList L;		//声明一个顺序表
	InitList(L);	//初始化顺序表
	//尝试“违规”打印整个data数组
	for(int i = 0; i < MaxSize; i++)
		printf("data[%d] = %d\n", i, L.data[i]);
	//……未完待续,后续操作
	return 0;
}	
  • 执行main函数,按照定义的数据结构为L分配一个10*4B+4B的内存空间
    • 最大长度是10,此处数据类型是int,占4个字节,再加上int型的length占4个字节
  • 初始化L
    • 如果没有初始化数组,而只初始化长度的话就可能出现问题:内存中有遗留的“脏数据”,给数据元素填充了乱七八糟的数。
  • 其实真正操作过程中把各个数据元素设为默认值是可以省略的,因为一旦设置了length值,访问时for循环就得写成 for(int i = 0; i <L.length; i++),这时候我们发现length设了默认值0,糟糕,尝试打印就不能执行了,这个访问方式就不太OK
    • 最好的访问方式是使用基本操作来访问各个数据元素,即使用GetElem(L, i),把L线性表中的第i个元素取出来。
  • length的初始化工作不能省略,编译器会给int型变量给默认初始值,给多少由编译器决定,所以为了不出问题必须要自己给length初始化
  • 局限:静态分配方式下,顺序表的表长确定后就无法更改,存储空间是静态的,会造成溢出或浪费

动态分配

#include <stdlib.h>		//malloc、free 函数的头文件
#define InitSize 10		//顺序表的初始长度
typedef struct{
ElemType *data;			//指示动态分配数组的指针
int MaxSize;			//顺序表的最大容量
int length;				//顺序表的当前长度
}SeqList;				//顺序表的类型定义(动态分配方式)

//初始化
void InitList(SeqList &L){
//用 malloc 函数申请一片连续的存储空间
L.data = (int *)malloc(InitSize * sizeof(int));
L.length = 0;
L.MaxSize = InitSize;
}

//增加动态数组的长度
void IncreaseSize(SeqList &L, int len){
int *p = L.data;
L.data = (int *)malloc((L.MaxSize +len) * sizeof(int));
for(int i = 0; i < L.length; i++){
	L.data[i] = p[i];			//将数据复制到新区域
}
L.MaxSize = L.MaxSize + len;	//顺序表最大长度增加 len
free(p);						//释放原来的内存空间
}

int main(){
	SeqList L;
	InitList(L);
	//……往顺序表中随便插入几个元素……
	IncreaseSize(L, 5);
	return 0;
}
  • 该结构体中的指针指向顺序表中第一个数据元素
  • MaxSize是动态分配的
    • malloc, free函数(C语言)
    • new, delete (C++)
    L.data = (ElemType *) malloc (sizeof(ElemType) * InitSize);
    
    • malloc 函数动态分配一整片连续的存储空间,返回一个指针,指向该存储空间起始地址,此指针需要强制转换为你定义的数据元素类型指针
    • Tip1 malloc 函数的动态分配是指按照数据元素类型调节所分配地址空间的大小,而线性表的动态分配是指 可以在代码中让顺序表的最大长度增加以满足要求 这样的“动态”功能
    • Tip2所谓动态分配中的动态增加并不是在原有基础上扩展空间,而是另觅一块更大的空间,这样做时间复杂度比较高
    • Tip3realloc也可以实现上述功能

顺序表的特点

(用顺序存储的方式实现的线性表就叫顺序表,ok)
1.随机访问,即可以在O(1)时间内找到第i个元素 ——data[i-1]
2.存储密度高,每个节点只存储数据元素,没有指针的存储
3.拓展容量不方便
4.插入删除操作不方便,需要移动大量的元素

插入删除

ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e
注:此处均采用静态分配方式实现

#define MaxSize 10		//定义最大长度
typedef struct{
ElemType data[MaxSize];//用静态的“数组”存放数据元素 
int length;				//顺序表的当前长度
}SqList;				//顺序表的类型定义

bool ListInsert(SqList &L, int i, int e){
	if(i<1||i>L.length+1)	//判断i的范围是否有效
		return false;
	if(L.length>=MaxSize)	//当前存储空间已满,不能插入
		return false;
	for(int j=L.length; j>=i; i--)
							//将第i个元素及之后的元素后移
		L.data[j] = L.data[j-1];
	L.data[i-1]=e;			//在位置i处放入e
	L.length++;				//长度加1
	return true;
}
/*void ListInsert(SqList &L, int i, int e){
for(int j = L.length; j >= i; j--) 
//将第i个元素及之后的元素后移
	L.data[j] = L.data[j-1];
L.data[i-1] = e;		//在位置i处放入e
L.length++;				//长度加1
}
*/
bool ListDelete(SqList &L, int i, int &e){
	if(i<1||i>L.length)		//判断i的范围是否有效
		return false;
	e = L.data[i-1];		//将被删除的元素赋值给e
	for(int j=i; j<L.length; j++)//将第i个位置后的元素前移
		L.data[j-1] = L.data[j];
	L.length--;					//线性表长度减1
	return true;
int main(){
SqList L;		//生命一个顺序表
InitList(L);	//初始化顺序表
//……此处省略一些代码,插入几个元素
ListInsert(L, 3, 3);
int e = -1;
if(ListDelete(L, 3, e))
	printf("已删除第3个元素,删除元素值为=%d\n",e);
else
	printf("位序i不合法,删除失败\n");
return 0;
}
  • 插入操作的时间复杂度
    最好情况:O(1)
    最坏情况:O(n)
    平均情况:O(n) /假设新元素插入到任何一个位置的概率相同,即i =1,2,3,……,length+1的概率都是p = 1/n+1
    i=1循环n次,i=2,循环n-1次……i=n+1,循环0次
    平均循环次数 = np +(n-1)p+(n-2)p+……+1
    p = n/2*/
  • 删除操作的时间复杂度
    最好情况:O(1)
    最坏情况:O(n)
    平均情况:假设删除任何一个元素的概率相同,即i = 1,2,3,……,length的概率都是p = 1/n
    i=1,循环n-1次,i=2,循环n-2次,i=3,循环n-3次,……,i=n,循环0次,平均循环次数=(n-1)p+(n-2)p+……+1*p = (n-1)/2, O(n)

查找

按位查找

GetElem(L,i):按位查找操作,获取表L中第i个未知的元素的值

  • 采用静态分配方式:
#define MaxSize 10			//定义最大长度
typedef struct{
	
	ElemType data[MaxSize];		//用静态的“数组”存放数据元素
	int length;					//顺序表的当前长度
}SqList;					//顺序表的类型定义

ElemType GetElem(SqList L, int i){
	return L.data[i-1];		//和位序统一
}
  • 采用动态分配方式
#define InitSize 10			//顺序表的初始长度
typedef struct{
	ElemType *data;				//指示动态分配数组的指针
	int MaxSize;				//顺序表的最大容量
	int length;					//顺序表的当前长度
}SeqList;					//顺序表的类型定义(动态分配方式)

ElemType GetElem(SeqList L, int i){//和访问普通数组的方法一样
	return L.data[i-1];
}
//如果一个ElemType占6B,即sizeof(ElemType)==6
//指针data指向的地址为2000,即初始data[0]始于2000,
//data[1]始于2006,以此类推。"按位查找"
  • 时间复杂度:O(1)
    顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素——“随机存取”特性

按值查找

LocateElem(L, e):按值查找操作,在表L中查找具有给定关键字值的元素

#define InitSize 10			//顺序表的初始长度
typedef struct{
	ElemType *data;				//指示动态分配数组的指针
	int MaxSize;				//顺序表的最大容量
	int length;					//顺序表的当前长度
}SeqList;					//顺序表的类型定义(动态分配方式)

//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L, ElemType e){
	for(int i=0; i<L.length; i++)
		if(L.data[i] == e)//这里直接用==进行比较,后面会说明
			return i+1;	
			//数组下标为i的元素值等于e,返回其位序i+1
	return 0;//退出循环,说明查找失败	
}
LocateElem(L, 9);//调用
  • 结构类型的比较
typedef struct{
	int num;
	int people;
}Customer;

void test(){
	Customer a;
	a.num = 1;
	a.people = 1;
	Customer b;
	b.num = 1;
	b.people = 1;
	/*if(a == b){……}//这样是不对的*/
	if(a.num == b.num && a.people == b.people){
		printf("相等");
	}else{
		printf("不相等");
	}
}
  • 按值查找的时间复杂度
    最好情况:O(1)
    最坏情况:O(n)
    平均情况:假设目标元素出现在任何一个位置的概率相同,都是1/n, 目标元素在第1位,循环1次;在第2位,循环2次;……;在第n位,循环n次
    平均循环次数 = (n+1)/2, O(n)

链表

单链表

  • 不支持随机存取
typedef struct LNode{		//定义单链表结点类型
	ElemType data;			//每个节点存放一个数据元素(数据域)
	struct LNode *next;		//指针指向下一个节点
}LNode, *LinkList;			//typedef数据类型重命名
LNode *p = (LNode*)malloc(sizeof(LNode));
//增加一个新的结点,p指向新申请的空间
typedef struct LNode LNode;
//把struct LNode重命名为LNode
typedef struct LNode *LinkList;
// 要表示一个电链表时,只需声明一个头指针L,指向单链表的第一个结点
LNode * L;//声明一个指向单链表第一个结点的指针
//LinkList L;//声明一个指向单链表第一个结点的指针(可读性好些)
//本质上LNode*和LinkList是一个东西,但是LinkList强调这是一个单链表,LNode*强调这是一个结点
LNode * GetElem(LinkList L, int i){
	int j = 1;
	LNode *p = L->next;
	if(i==0)
		return L;
	if(i<1)
		return NULL;
	while(p!=NULL && j<i){
		p = p->next;
		j++;
	}
	return p;
}
//下面是头插法插入结点的例子,可再体会一下
LinkList List_HeadInsert(LinkList &L){
逆向建立单链表//
	LNode *s; int x;
	L = (LinkList)malloc(sizeof(LNode));
	//创建头结点
	L->next = NULL;
	//初始为空链表
	scanf("%d", &x);
	//输入结点的值
	while(x != 9999){
		s = (LNode*)malloc(sizeof(LNode));
		//创建新结点
		s->data = x;
		s->next = L->next;
		L->next = s;
		//将新结点插入表中,L为头指针
		scanf("%d", &x);
	}
	return L;
}
//下面是不带头结点的单链表的初始化工作
bool InitList(LinkList &L){
	L = NULL;//防止脏数据
	return true;
}
void test1(){
	LinkList L;
	//并没有创建结点,只是声明一个指向单链表的指针
	InitList(L);
	//……
}
	//下面写个判空法一:
bool Empty (LinkList L){
	if (L == NULL)
		return true;
	else
		return false;
}
//再写个判空法二:
bool Empty(LinkList L){
	return (L == NULL);
}
//下面是带头结点的单链表的初始化工作
bool InitList(LinkList &L){
	L = (LNode*)malloc(sizeof(LNode));
	if(L==NULL)
		return false;
		//内存不足,分配失败
	L->next = NULL;//暂时这个单链表的头结点屁股后面啥也没有
	return true;
}
void test2(){
	LinkList L;
	InitList(L);
	//……
bool Empty(LinkList L){
	if(L->next == NULL)
		return true;
	else
		return false;
}
  • 带不带头结点呢?
    答:带嘛。不带头结点时对第一个数据结点和后续结点的处理需要用不同的代码逻辑,对空表和非空表的处理也是。(麻烦)因为不带头结点的:头指针直接指向存数据元素的结点;带头结点的:头指针指向头结点,头结点不存放数据元素,再往后的结点才开始存。

插入删除

按位序插入(不带头结点)

ListInsert(&L, i, e):在表L中的第i个位置上插入指定元素e
找到第i-1个结点,将新结点插入其后,故当i=1时需要特殊处理

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode,*LinkList;
//在第i个位置上插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
	if(i == 1){//插入第1个结点的操作与其他结点操作不同
		LNode *s = (LNode*)malloc(sizeof(LNode));
		s->data = e;
		s->next = L;
		L = s;//头指针指向新结点
		return true;
	}
	LNode *p;//指针p指向当前扫描到的结点
	int j = 1;//当前p指向的是第几个结点
	p = L;//L指向头结点,不是头结点
	while(p!=NULL && j<i-1){//循环找到第i-1个结点
		p = p->next;
		j++;
	}
	if(p == NULL)//i的值不合法
		return false;
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;//将结点s连到p之后
	return true;//插入成功
}
  • 分析
    1.如果i = 1(插在表头)
    因为不带头结点,则插入删除第一个元素时,需要更改头指针L
    2.如果i>1除了第一次j=1外其余步骤跟带头结点相同
按位序插入(带头结点)

ListInsert(&L, i, e):在表L中的第i个位置上插入指定元素e

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode,*LinkList;
//在第i个位置上插入元素e
bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
	LNode *p;//指针p指向当前扫描到的结点
	int j = 0;//当前p指向的是第几个结点,第0个结点不存数
	p = L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p!=NULL && j<i-1){//循环找到第i-1个结点
		p = p->next;
		j++;
	}
	//以下代码可由return InsertNextNode(p, e);代替,即调用指定结点的后插操作,这将在后一part讲到
	if(p == NULL)//i的值不合法
		return false;
	LNode *s = (LNode *)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;//将结点s连到p之后
	return true;//插入成功
}
  • 分析
    1.如果i = 1(插在表头)
    最好时间复杂度O(1)
    2.如果i = 3(插在表中)
    先找到i-1,再插入
    3.如果i = 表长(插在表尾)
    最坏时间复杂度O(n)
    n为表长
指定结点的后插操作
//后插操作:在p结点之后插入元素e
typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;
bool InsertNextNode(LNode *p, ElemType e){
	if(p == NULL)
		return false;
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if(s ==NULL)//内存分配失败
		return false;
	s->data = e;//用结点s保存数据元素e
	s->next = p->next;
	p->next = s;
	//将结点s连到p之后
	return true;
}
指定结点的前插操作

bool InsertPriorNode(LNode *p, ElemType e)
这里需要加上传入头指针操作才能找到p结点的前驱结点,即bool InsertPriorNode(LinkList L, LNode *p, ElemType e),循环查找p的前驱结点q,再对q后插e
时间复杂度为O(n)

//对于需要传入头指针的思路具有局限性,因为头指针不一定能够得到。
//下面是另一种灵活的前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElemType e){
	if (p == NULL)
		return false;
	LNode *s = (LNode*)malloc(sizeof(LNode));
	if (s == NULL)//内存分配失败
		return false;
	s->next = p->next;
	p->next = s;//将新结点 s 连到 p 之后
	s->data = p->data;//将 p 中元素复制到 s 中
	p->data = e;//p 中元素覆盖为 e 
	return true;
}

下面是王道书中的版本(本质相同):

bool InsertPriorNode(LNode *p, LNode *s){
	if (p == NULL || s == NULL)
		return false;
	s->next = p->next;
	p->next = s;//s连到p之后
	ElemType temp = p->data;//交换数据域部分
	p->data = s->data;
	s->data = temp;
	return true;
}
按位序删除(带头结点)

ListDelete(&L, i, &e):删除L中第i个位置的元素,并用e返回删除元素的值
找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;
bool ListDelete(LinkList &L, int i, ElemType &e){
	if(i<1)
		return false;
	LNode *p;//指针p指向当前扫描到的结点
	int j = 0;//当前p指向的是第几个结点
	p = L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p != NULL && j<i-1){
		p = p->next;
		j++;
	}
	if(p == NULL)//i值不合法
		return false;
	if(p->next == NULL)//第i-1个结点之后已无其他结点
		return false;
	LNode *q = p->next;//令Q指向被删除结点
	e = q->data;//用e返回元素的值
	p->next = q->next;//将*q结点从链中“断开”
	free(q);//释放结点的存储空间
	return true;//删除成功
}
  • 分析
    最坏、平均时间复杂度O(n)
    最好时间复杂度:O(1)
指定结点的删除

删除结点p,需要修改其前驱结点的next指针
法一:传入头指针,循环寻找p的前驱结点
法二:偷天换日(类似于结点前插的实现)

//偷天换日法删除指定结点p
bool DeleteNode(LNode *p){
	if (p == NULL)
		return false;
	LNode *q = p->next;//令q指向*p的后继结点
	p->next = q->next;//和后继结点交换数据域
	free(q);//将*q结点从链中“断开”
	return true;//释放后继结点的存储空间
}
  • 分析
  • 如果p是最后一个结点时,只能从表头开始依次寻找p的前驱,时间复杂度是O(n)

查找

按位查找

GetElem(L,i):按位查找,获取表L中第i个位置的元素的值(前述单链表的按位删除操作用到了按位查找,查找第 i-1 个元素LNode*p = GetElem(L, i-1))
LocateElem(L, e):按值查找,在表L中查找具有给定关键字值的元素

LNode * GetElem(LinkList L, int i){
	if(i<0)
		return NULL;
	LNode *p;//指针p指向当前扫描到的结点
	int j = 0;//当前p指向的是第几个结点
	p = L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p!=NULL && j<i){//循环找到第i个结点
		p = p->next;
		j++;
	}
	return p;
}
  • 分析
    平均时间复杂度:O(n)
    王道书版本:
LNode *GetElem(LinkList L, int i){
	int j = 1;
	LNode * = L->next;
	if(i == 0)
		return L;
	if(i<1)
		return NULL;
	while(p!=NULL && j<i){
		p = p->next;
		j++;
	}
	return p;
}
按值查找
//按值查找,找到数据域==e 的结点
LNode * LocateElem(LinkList L, ElemType e){
	LNode *p = L->nexr;
	//从第1个结点开始查找数据域为e的结点
	while (p != NULL && p->data != e)
		p = p->next;
	return p;//找到后返回该结点指针,否则返回NULL
}
  • 分析
    平均时间复杂度:O(n)
求表长
//求表的长度
int length(LinkList L){
	int len = 0;//统计表长
	LNode *p = L;
	while(p->next != NULL){
		p = p->next;
		len++;
	}
	return len;
}
  • 分析
    平均时间复杂度:O(n)

单链表的建立

尾插法

1.初始化单链表
2.设置变量length 记录链表长度
3.while循环{
每次取一个数据元素e;
ListInsert(L, length+1, e)插到尾部
length++;
}

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LNode, *LinkList;
//初始化一个单链表(带头结点)
bool InitiList(LinkList &L){
	L = (LNode*)malloc(sizeof(LNode));
	if(L==NULL)//内存不足,分配失败
		return false;
	L->next = NULL;//头结点之后暂时还没有结点
	return true;
}
void test(){
	LinkList L;//声明一个指向单链表的指针
	//初始化一个空表
	InitList(L);
	//……
}
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
	LNode *p;//指针p指向当前扫描到的结点
	int j=0;//当前p指向的是第几个结点
	p = L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p != NULL && j<i-1){
		p = p->next;
		j++;
	}
	if(p==NULL)//i值不合法
		return false;
	LNode *s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;//将结点s连到p之后
	return true;//插入成功
}
  • 分析
    插入第n个元素时循环n-1次,0+1+2+3+……+n-1=O(n^2)
    设置一个表尾指针来改善
LinkList List_TailInsert(LinkList &L){//正向建立单链表
	int x;//设ElemType为整型
	L = (LinkList)malloc(sizeof(LNode));//建立头结点
	LNode *s,*r = L;//r为表尾指针
	scanf("%d", &x);//输入结点的值
	while(x!=9999){//输入9999表示结束
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		r->next = s;
		r = s;		//r指向新的表尾结点
		scanf("%d", &x);
	}
	r->next = NULL;//尾结点指针置空
	return L;
}
  • 分析
    时间复杂度O(n)
头插法
  • 对头结点执行后插操作
    1.初始化单链表
    2.while循环{
    每次取一个数据元素e;
    InsertNextNode(L, e);
    }
LinkList List_HeadInsert(LinkList &L){//逆向建立链表
	LNode *s;
	int x;
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;//初始为空链表,防止脏数据
	scanf("%d", &x);//输入结点的值
	while(x!=9999){
		s = (LNode*)malloc(sizeof(LNode));
		s->data = x;
		s->next = L->next;
		L->next = s;//将新结点插入表中,L为头指针
		scanf("%d", &x);
	}
	return L;
}
  • 分析
    头插法可实现链表的逆置
    例题:《数据结构题集》严奶奶

双链表

单链表无法逆向检索,双链表可进可退,存储密度稍低

typedef struct DNode{//定义双链表结点类型
	ElemType data;//数据域
	struct DNode *prior, *next;//前驱和后继指针
}DNode, *DLinklist;
//DLinklist与DNode*等价
//双链表的初始化(带头结点)
bool InitDLinkList(DLinklist &L){
	L = (DNode*)malloc(sizeof(DNode));//分配一个头结点
	if (L==NULL)//内存不足,分配失败
		return false;
	L->prior = NULL;//头结点的prior 永远指向NULL
	L->next =NULL;//头结点之后暂时还没有结点
	return true;
}
void testDLinkList(){
	//初始化双链表
	DLinklist L;
	InitDLinkList(L);
	//……
}
//判断双链表是否为空
bool Empty(DLinklist L){
	if (L->next ==NULL)
		return true;
	else
		return false;
}
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){
	if (p==NULL || s==NULL)
		return false;
	s->next = p->next;//将结点*s插入到结点*p之后
	if (p->next != NULL)//如果p结点有后继结点
		p->next->prior = s;
	s->prior = p;
	p->next  = s;
	return true;
}
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
	if (p == NULL)
		return false;
	DNode *q = p->next;	//找到p的后继结点q
	if (q == NULL)		//p没有后继
		return false;
	p->next = q->next;
	if (q->next != NULL)	//q结点不是最后一个结点
		q->next->prior = p;
	free(q);				//释放结点空间
	return true;
}
//销毁双链表
void DestoryList(DLinklist &L){
	//循环释放各个数据结点
	while(L->next != NULL)
		DeleteNextDNode(L);
	free(L);	//释放头结点
	L = NULL;	//头指针指向NULL
}
//遍历双链表(后向)
while (p!=NULL){
	//对结点p做相应处理,如打印
	p = p->next;
}
//前向遍历只需把 next 改为 prior 即可
//前向遍历(跳过头结点)
while(p->prior != NULL){//只想处理数据结点,不想处理头结点
	//对结点p做相应处理
	p = p->prior;
}
  • 分析
    双链表不可随机存取,按位查找(加入计数器,记录此时指向哪个的元位序素),按值查找(对当前指向的结点进行值的对比)操作都只能用遍历的方式实现。时间复杂度O(n)

循环链表

循环单链表:表尾结点的next指针指向头结点

typedef struct LNode{	//定义单链表结点类型
	ElemType data;		//每个结点存放一个数据元素
	struct LNode *next;	//指针指向下一个结点
}LNode, *LinkList;

//初始化一个循环链表
bool InitList(LinkList &L){
	L = (LNode *)malloc(sizeof(LNode));//分配一个头结点
	if(L==NULL)//内存不足,分配失败
		return false;
	L->next=L;//头结点next指向头结点
	return true;
}
//判断循环单链表是否为空
bool Empty(LinkList L){
	if (L->next == L)
		return true;
	else
		return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,LNode *p){
	if (p->next==L)
		return true;
	else
		return false;
}

循环双链表:表头结点的prior指向表尾结点,表尾结点的next指向头结点

typedef struct DNode{
	ElemType data;
	struct DNode *prior, *next;
}DNode, *DLinkList;
//初始化空的循环双链表
bool InitDLinkList(DLinkList &L){
	L=(DNode*)malloc(sizeof(DNode));
	if(L==NULL)//内存不足,分配失败
		return false;
	L->prior = L;	//头结点的prior指向头结点
	L->next =L;	//头结点的next指向头结点
	return true;
}
void testDLinkList(){
	//初始化循环双链表
	DLinkList L;
	InitDLinkList(L);
	//……
}
//判断循环双链表是否为空
bool Empty(DLinkList L){
	if (L->next==L)
		return true;
	else
		return false;
}
//判断p结点是否为循环双链表的表尾结点
bool isTail(DLinkList L, DNode *p){
	if(p->next==L)
		return true;
	else
		return false;
}
//双链表的插入
//在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){
	s->next = p->next;
	p->next->prior = s;
	s->prior = p;
	p->next = s;
}
//删除p的后继结点q
p->next = q->next;
q->next->prior = p;
free(q);

静态链表

  • 分配一整片连续的内存空间,各个结点集中安置
  • 由数据元素和下一个结点的数组下标(游标)组成
  • 0号结点充当头结点
  • 游标值为-1表示已经到达表尾
  • eg:每个数据元素4B,每个游标4B(每个结点共8B)
    设起始地址为addr
    则e1的存放地址为addr + 8*2
    在这里插入图片描述
#define MaxSize 10	//静态链表的最大长度
struct Node{		//静态链表结构类型的定义
	ElemType data;	//存储数据元素
	int next;		//下一个元素的数组下标
};
void testSLinkList(){
	struct Node a[Maxsize];
	//
}
//另一种写法
#define Maxsize 10
typedef struct{
	ElemType data;
	int next;
}SLinkList[Maxsize];
//等价于下面这种写法
#define Maxsize 10
struct Node{
	ElemType data;
	int next;
};
typedef struct Node SLinkList[Maxsize];
//用SLinkList定义一个长度为MaxSize的Node型数组
//使用(对应上面两个定义)
void testSLinkList(){
SLinkList a;
//
}
void testSLinkList(){
	struct Node a[Maxsize];
	//a是一个Node型的数组

对上面三种定义的理解验证

#define MaxSize 10
struct Node{
	int data;
	int next;
};
typedef struct{
	int data;
	int next;
}SLinkList[MaxSize];
void testSLinkList(){
	struct Node x;
	pintf("sizeX = %d\n", sizeof(x));
	struct Node a[MaxSize];
	printf("sizeA = %d\n", sizeof(a));
	SLinkList(b);
	printf("sizeB = %d\n", sizeof(b));
}

运行结果:
sizeX = 8
sizeA = 80
sizeB = 80
正经的基本操作

//初始化,把a[0]的next设为-1,即NULL
//插入位序为i的结点:
 -找到一个空的结点,存入数据元素
 -从头结点出发找到位序为i-1的结点
 -修改新结点的next
 -修改i-1号结点的next
//判断结点为空:
-空闲结点的next设为-2,表示空闲
//删除某个结点
-从头结点出发找到前驱结点
-修改前驱结点的游标
-删除结点next设为-2

-分析
优缺点很显然,增删不用大量移动,但不能随机存取,只能从头结点开始依次往后查找,容量固定不变

总结

最后来一道开放式例题:
描述顺序表和链表的blabla……
顺序表和链表的逻辑结构都是线性结构,都属于线性表。
但是二者的存储结构不同,顺序表采用顺序存储(特点,带来的优缺点);链表采用链式存储结构(特点,带来的优缺点)
由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时……;当插入一个数据元素时……,当删除时……,当查找时……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值