[数据结构笔记]顺序表与链表

线性表

线性表(linear list)是多个具有相同特性的数据元素构成的有限序列。
线性表是一种被广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串等。
线性表在逻辑上是线性结构,即连续的一条线。线性指的是逻辑上的结构是线性连续的,而其在物理结构上并不一定是连续的。线性表的存储在物理层面上通常以数组(顺序表)和链式结构(链表)的形式实现。

顺序表

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。

顺序表一般可以分为:

1.静态顺序表:使用定长数组存储元素。
2.动态顺序表:使用动态开辟的数组存储。

顺序表的优缺点

优点:
1.支持随机访问(二分查找、优化的快排等需要随机访问)。
缺点:
1.对于中间/头部的插入删除的操作,所需的时间复杂度为O(N)。
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的性能开销。
3.增容一般固定按2倍增加,势必会有不小的空间浪费。

动态顺序表及其实现

静态顺序表只适用于已明确知道需要存多少数据的场景:
定长数组导致扩容时容易出现“空间开多了浪费,开少了不够用”的情况。
所以这里我们讨论能根据需求动态地分配空间大小的动态顺序表:

头文件 SeqList.h

#pragma once
#include
#include
#include

typedef int SLDataType;

//动态顺序表
typedef struct SeqList {
	SLDataType* head;//声明长度未定的动态数组
	int size;//表示所存储的元素数量,不是字节数
	int capacity;//数组的容量
}SL;

//接口函数
void SeqListPrint(SL* ps);
void SeqListInit(SL* ps);
void SeqListDestroy(SL* ps);
void SeqListCheckCapacity(SL* ps);

int SeqListFind(SL* ps, SLDataType x);//找到返回下标,没找到返回-1

void SeqListInsert(SL* ps, int pos, SLDataType x);//指定下标位置插入,包含头插尾插的功能
void SeqListErase(SL* ps, int pos);//指定下标位置删除,包含头删尾删的功能

//复用insert,erase函数即可实现
void SeqListPushBack(SL* ps, SLDataType x);
void SeqListPopBack(SL* ps);
void SeqListPushFront(SL* ps, SLDataType x);
void SeqListPopFront(SL* ps);

接口实现 SeqList.c

#include"SeqList.h"

void SeqListPrint(SL* ps) {
	for (int i = 0; i < ps->size; ++i) {
		printf("%d", ps->head[i]);
	}
	printf("\n");
}

void SeqListInit(SL* ps) {
	ps->head = NULL;
	ps->size = ps->capacity = 0;
}

void SeqListDestroy(SL* ps) {
	free(ps->head);//释放head所代表的开辟出的空间
	ps->head = NULL;//置空head
	ps->capacity = ps->size = 0;//归零capacity和size
}

//检查是否需要增容并根据情况进行相应的增容操作
void SeqListCheckCapacity(SL* ps) {
	if (ps->size == ps->capacity) {//所存元素数量达到可用容量上限
		//决定新容量:
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;//还没开空间就先开4字节空间,若是空间不足而扩容就扩2倍空间
		//增容:
		SLDataType* tmp = (SLDataType*)realloc(ps->head, newcapacity * sizeof(SLDataType));//当head为NULL,realloc对NULL增容相当于malloc
		//检查增容结果:
		if (tmp == NULL) {//tmp作为realloc的返回值指向扩容的内存块,若返回0(即NULL)意味着扩容失败
			printf("realloc failed\n");
			exit(-1);//正常退出是0,异常退出是-1
		}
		//交付增容成果:
		ps->head = tmp;
		ps->capacity = newcapacity;
	}
}

//遍历查找元素,找到返回下标,没找到返回-1
int SeqListFind(SL* ps, SLDataType x) {
	for (int i = 0; i < ps->size; i++) {
		if (ps->head[i] == x) {
			return i;
		}
	}
	return -1;
}

//指定下标位置插入
void SeqListInsert(SL* ps, int pos, SLDataType x) {
	assert(pos >= 0 && pos <= ps->size);//检查下标合法性
	SeqListCheckCapacity(ps);//判断ps的状况并按需对其执行相应的动作(开空间或满容量时增容)
	int end = ps->size - 1;//确定末位下标,以之为迁移操作的起点
	while (end >= pos) {//从末位开始向前直到pos下标处,将元素逐个向后迁移一位,以使得pos处空出,可供插入 
		ps->head[end + 1] = ps->head[end];
		--end;
	}
	ps->head[pos] = x;//将要插入的元素放入pos位置
	ps->size++;//完成前述操作后再记录本次插入造成的元素数量增长
}

//指定下标位置删除
void SeqListErase(SL* ps, int pos) {
	assert(pos >= 0 && pos < ps->size);//下标size指向末尾元素的下一位,也就是无元素的位置。插入位置等于size则相当于尾插,而删除位置等于size就无意义了。
	int begin = pos + 1;//pos的下一位作为迁移操作的起点:begin
	while (begin < ps->size) {//从begin位置开始直到size下标的前一位,将元素逐个向前迁移一位,以覆盖begin位置的要删除的元素
		ps->head[begin - 1] = ps->head[begin];
		++begin;
	}
	ps->size--;//完成前述操作后再记录本次删除造成的元素数量减少
}

//复用insert,erase函数即可实现头尾的插入删除
void SeqListPushBack(SL* ps, SLDataType x) {
	SeqListInsert(ps, ps->size, x);//在size下标处插入便是尾插
}

void SeqListPopBack(SL* ps) {
	SeqListErase(ps, ps->size - 1);//删除size下标的前一位元素便是尾删
}

void SeqListPushFront(SL* ps, SLDataType x) {
	SeqListInsert(ps, 0, x);//在下标0处插入便是头插
}//顺序表头插开销较大,因为所有元素都需要向后迁移

void SeqListPopFront(SL* ps) {
	SeqListErase(ps, 0);//删除下标0处的元素便是头删
}

//非复用insert与erase的头尾插入删除
//void SeqListPushBack(SL* ps, SLDataType x) {
//	SeqListCheckCapacity(ps);
//	ps->head[ps->size] = x;
//	ps->size++;
//	SeqListInsert(ps, ps->size, x);
//}
//void SeqListPopBack(SL* ps) {
//	assert(ps->size > 0);
//	ps->size--;
//}
//void SeqListPushFront(SL* ps, SLDataType x) {
//	SeqListCheckCapacity(ps);
//	int end = ps->size - 1;
//	while (end>0){
//		ps->head[end + 1] = ps->head[end];
//		--end;
//	}
//	ps->head[0] = x;
//	ps->size++;
//}
//void SeqListPopFront(SL* ps) {
//	assert(ps->size > 0);
//	int begin = 1;
//	while (beginsize){
//		ps->head[begin - 1] = ps->head[begin];
//		++begin;
//	}
//	ps->size--;
//}

链表

链表的概念与结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

链表的优缺点

优点:
1.按需申请空间,比顺序表更少的空间浪费。
2.插入删除数据的操作不需要挪动数据。
缺点:
1.每个节点都需要存放别的节点的地址。
2.不支持随机访问(直接访问指定下标位置的元素)。

单向无头非循环链表(单链表)与双向带头循环链表

-无头单向非循环链表:结构简单,一般不单独用于存储数据。实际应用中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。笔试面试高频考点。
-.带头双向循环链表:结构最复杂,一般用于单独存储数据。实际中单独使用的链表数据结构,都是带头双向循环链表。结构复杂但是便于实现。

单链表的实现与增删查改

头文件SList.h

#pragma once
#include 
#include 
#include 

typedef int SLTDataType;

typedef struct SListNode {
	SLTDataType data;//本节点元素
	struct SListNode* next;//下一节点的地址
}SLTNode;

SLTNode* BuySLTNode(SLTDataType x);//动态申请一个节点并为其元素赋值x
SLTNode* CreateSLT(int n);//创建单链表,共n个节点
void SLTPrint(SLTNode* plist);//打印
SLTNode* SLTFind(SLTNode* plist, SLTDataType x);//查找
void SLTDestroy(SLTNode** pplist);//销毁单链表

void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在pos位置之后插入元素值为x的节点
void SLTEraseAfter(SLTNode* pos);//删除pos位置之后的值

void SLTPushBack(SLTNode** pplist, SLTDataType x);//尾插
void SLTPushFront(SLTNode** pplist, SLTDataType x);//头插
void SLTPopBack(SLTNode** pplist);//尾删
void SLTPopFront(SLTNode** pplist);//头删

接口实现 SList.c

#include"SList.h"

//动态申请一个节点并为其赋值x
SLTNode* BuySLTNode(SLTDataType x) {
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//申请新节点的空间
	if (newnode == NULL) {//空间申请结果检查
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;//为节点赋值x
	newnode->next = NULL;//新节点的下一位先指向空
	return newnode;//返回新节点的地址
}

//创建单链表,共n个节点
SLTNode* CreateSLT(int n) {
	//初始化(注意这里的phead与ptail只在本函数内作标记用,不存在于节点内,这是单链表)
	SLTNode* phead = NULL;
	SLTNode* ptail = NULL;
	int x = 0;
	//创建与连接节点
	for (int i = 0; i < n; ++i) {
		SLTNode* newnode = BuySLTNode(i);//这里作为示例,默认为第i个节点赋初值i
		if (phead == NULL) {//若尚未创建节点,则创建一个,创建此节点后链表的头尾均为该节点
			ptail = phead = newnode;
		}
		else {//若已有节点,则将尾节点的next指向新节点,然后新节点就成了尾节点
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;//返回创建的单链表
}

//打印:遍历并打印每个元素
void SLTPrint(SLTNode* plist) {//指向目标链表头节点地址的指针传给本函数的形参
	SLTNode* cur = plist;
	while (cur != NULL) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

//查找:遍历并对比每个元素与待找元素的值,找到则返回找到的元素所在节点的地址,遍历完没找到则返回空
SLTNode* SLTFind(SLTNode* plist, SLTDataType x) {
	SLTNode* cur = plist;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}



//销毁单链表
void SLTDestroy(SLTNode** pplist) {
//↑由于需要改变“存放了目标链表头节点地址的指针变量”的值(free后要置空),不能将这个一级指针变量的值直接传给形参后使用,
//因为形参是实参的拷贝,改变形参的值并不会影响实参的值。
//要使用函数改变一个变量所保存的值,就不能传递这个变量所保存的值本身,而必须传这个变量的地址,“保存指针变量的地址的指针变量”便是二级指针。
	SLTNode* cur = *pplist;//cur指向待销毁的链表头部
	while (cur) {//遍历并逐个释放节点
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pplist = NULL;//释放完成后置空头节点的指针。后续每个节点的访问都依赖其之前的节点,后续节点自身没有需要置空的指针,所以这里只需要置空头节点。
	//↑对“SLTNode**”类型的二级指针pplist解引用(*pplist),找到“存放了目标链表头节点地址的指针变量”的地址,以进行赋值。
}

//在pos位置之后插入元素值为x的节点 
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
//为什么不在pos位置之前插入?因为单链表节点不保存前一个节点的地址,从pos找前一个比较麻烦
	assert(pos);//确保pos指向的节点存在
	SLTNode* newnode = BuySLTNode(x);//创建新节点并记录位置
	newnode->next = pos->next;//先将新节点的下一位设为pos节点的下一位
	pos->next = newnode;//再将pos节点的下一位设为新节点,插入完成
}

//删除pos位置之后的值
void SLTEraseAfter(SLTNode* pos) {
//为什么不删除pos位置?根本原因同上,单链表节点不保存前一个节点的地址
	assert(pos);//确保pos指向的节点存在
	if (pos->next == NULL) {//若pos后无节点,则不进行操作,直接返回
		return;
	}
	else {//若pos后存在节点:
		SLTNode* nextNode = pos->next;//记录要释放的pos位置节点后一个节点的地址
		pos->next = nextNode->next;//将pos位置节点的下下个节点设为pos位置节点的下一个节点
		free(nextNode);//释放原本pos位置的后一个节点
	}
}

//链表不像顺序表那样支持用下标随机访问,也不能向前遍历,所以不适合像顺序表那样直接复用任意位置插入删除来实现头尾的插入删除
//二级指针相关内容已于前文SLTDestroy处说明,后文不再复述

//尾插
void SLTPushBack(SLTNode** pplist, SLTDataType x) {
	SLTNode* newnode = BuySLTNode(x);//创建新节点并记录位置
	if (*pplist == NULL) {//若链表中尚无节点,则该新节点尾插成为链表的头节点
		*pplist = newnode;
	}
	else {//若链表中已有节点:
		SLTNode* tail = *pplist;//设置tail标记,从链表头部开始遍历,直到所指节点的下一位为空,此时tail指向尾节点
		while (tail->next) {
			tail = tail->next;
		}
		tail->next = newnode;//将尾节点的下一位设为新节点的地址
	}
}

//头插
void SLTPushFront(SLTNode** pplist, SLTDataType x) {
	SLTNode* newnode = BuySLTNode(x);//创建新节点并记录位置
	newnode->next = *pplist;//将新节点的下一位设为头节点的地址
	*pplist = newnode;//以新节点代替之前的头节点,成为新的头节点
	//链表的头插效率远高于顺序表
}

//尾删
void SLTPopBack(SLTNode** pplist) {
	assert(*pplist);//确保链表不为空
	if ((*pplist)->next == NULL) {//当链表只有一个节点,释放该节点并置空即可,相当于销毁链表
		free(*pplist);
		*pplist = NULL;
	}
	else {//当链表不为空:
		SLTNode* tail = *pplist;//设置tail标记,从链表头部开始遍历
		while (tail->next->next) {//逐个节点遍历,直到下个节点的next为空,则tail所指节点的下个节点为尾节点(tail所指的节点暂时还不是尾节点)
			tail = tail->next;
		}
		free(tail->next);//释放尾节点
		tail->next = NULL;//tail所指节点的下一个节点设为空,此时tail所指节点成为了尾节点
	}
}

//头删
void SLTPopFront(SLTNode** pplist) {
	assert(*pplist);//确保链表不为空
	SLTNode* next = (*pplist)->next;//记录当前头节点的下一个节点的地址
	free(*pplist);//释放头节点
	*pplist = next;//令之前的第二个节点成为头节点
	//链表的头删效率也远高于顺序表
}

带头双向循环链表的实现与增删查改

头文件DList.h

#pragma once

#include
#include
#include
#include

typedef int LTDataType;

typedef struct ListNode {
	LTDataType data;//节点中的元素
	struct ListNode* prev;//前一个节点的地址
	struct ListNode* next;//后一个节点的地址
}LTNode;

LTNode* BuyLTNode(LTDataType x);//动态申请一个节点并为其元素赋值x
LTNode* LTInit();//初始化
void LTDestroy(LTNode* phead);//销毁
void LTPrint(LTNode* phead);//打印
LTNode* LTFind(LTNode* phead, LTDataType x);//查找 
bool LTEmpty(LTNode* phead);//检测是否为空
size_t LTSize(LTNode* phead);//检查节点数量

void LTInsert(LTNode* pos, LTDataType x);//pos前插入值为x的新节点
void LTErase(LTNode* pos);//删除pos位置的节点

//由于保存了前一个节点的位置,可复用LTInsert与LTErase实现头尾的插入删除
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);

接口实现 DList.c

#include"DList.h"

//动态申请一个节点并为其元素赋值x
LTNode* BuyLTNode(LTDataType x) {
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));//申请新节点
	if (node == NULL) {//新节点申请结果检查
		perror("malloc failed");
		exit(-1);
	}
	node->data = x;//为新节点元素赋值x
	node->prev = NULL;//新节点的前一位先指向空
	node->next = NULL;//新节点的下一位先指向空
	return node;//返回新节点的地址
}

//初始化
LTNode* LTInit() {//←用“返回节点地址而无参数”的形式,替代“无返回值并使用二级指针作为参数”的形式(参考单链表部分各函数的形式)。
//带头双向循环链表的哨兵位头节点初始时前后都指向自身,这个哨兵位头节点默认不保存有效数据。
//代表一个链表的指针变量永远指向哨兵位,除销毁链表外的任何基本操作都不会影响哨兵位,
//而销毁操作也可以像free函数那样把置空操作交给使用者进行,故该结构可以不像单链表那样用一堆二级指针
	LTNode* phead = BuyLTNode(-1);//这里随便给了个-1的值,无特殊含义。
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

//销毁
void LTDestroy(LTNode* phead) {
	assert(phead);//确保phead不为空
	LTNode* cur = phead->next;//从头节点的后一位开始,向后依次释放
	while (cur != phead) {
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);//释放phead
	//这里不需要phead = NULL;因为这里的phead只是形参,置空也不会影响实参
	//可以考虑改成传二级,或要求使用该函数的用户自行置空,就像free函数本身不负责置空那样
}

//打印
void LTPrint(LTNode* phead) {
	assert(phead);//确保phead不为空
	LTNode* cur = phead->next;//从phead的下一位,也就是第一位保存有效元素的节点开始遍历
	while (cur != phead) {//尾节点的下一位是哨兵位,若等于phead则已回到哨兵节点
		printf("%d", cur->data);//这里%d是因为默认data为int,按需修改
		cur = cur->next;
	}
	printf("\n");
}

//查找 
LTNode* LTFind(LTNode* phead, LTDataType x) {
//遍历并对比每个元素与待找元素的值,找到则返回找到的元素所在节点的地址,遍历完没找到则返回空
	assert(phead);//确保phead不为空
	LTNode* cur = phead->next;//从phead的下一位,也就是第一位保存有效元素的节点开始遍历
	while (cur != phead) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//检测是否为空
bool LTEmpty(LTNode* phead) {
	assert(phead);//确保phead不为空
	return phead->next == phead;//带头双向循环链表,只在无有效元素节点的情况下,头节点的下一位是头节点自己
}

//检查节点数量(不含哨兵位头节点)
size_t LTSize(LTNode* phead) {
	assert(phead);//确保phead不为空
	size_t size = 0;//初始化计数器 
	LTNode* cur = phead->next;//从phead的下一位,也就是第一位保存有效元素的节点开始遍历
	while (cur != phead) {//每经过一位,计数器加1,回到哨兵位头节点时终止循环
		size++;
		cur = cur->next;
	}
	return size;//返回计数器的值
}

//pos前插入值为x的新节点
void LTInsert(LTNode* pos, LTDataType x) {
	assert(pos);//确保pos指向有效的节点
	LTNode* prev = pos->prev;//找到并记录pos位置的前一个节点,记作prev
	LTNode* newnode = BuyListNode(x);//创建新节点
	prev->next = newnode;//将prev节点的下一位设为新节点
	newnode->prev = prev;//将新节点的前一位设为prev节点
	newnode->next = pos;//将新节点的下一位设为pos位置的节点
	pos->prev = newnode;//将pos位置的节点的前一位设为新节点
}

//删除pos位置的节点
void LTErase(LTNode* pos) {
	assert(pos);//确保pos指向有效的节点
	LTNode* prev = pos->prev;//记录pos位置节点的前一位,记作prev
	LTNode* next = pos->next;//记录pos位置节点的后一位,记作next
	free(pos);//释放pos位置的节点
	prev->next = next;//将prev节点的下一位设为next节点
	next->prev = prev;//将next节点的前一位设为prev节点
}


//由于保存了前一个节点的位置,便于复用LTInsert与LTErase实现头尾的插入删除

void LTPushBack(LTNode* phead, LTDataType x) {
	LTInsert(phead, x);//在头节点之前插入便是尾插
}

void LTPopBack(LTNode* phead) {
	LTErase(phead->prev);//删除头节点的前一位便是尾删
}

void LTPushFront(LTNode* phead, LTDataType x) {
	LTInsert(phead->next, x);//在头节点的后一位之前插入便是头插
}

void LTPopFront(LTNode* phead) {
	LTErase(phead->next);//删除头节点的后一位便是头删
}


//非复用LTInsert与LTErase的头尾插入删除
//void LTPushBack(LTNode* phead, LTDataType x) {
//	assert(phead);
//	LTNode* newnode = BuyLTNode(x);
//	LTNode* tail = phead->prev;
//	tail->next = newnode;
//	newnode->prev = tail;
//	newnode->next = phead;
//	phead->prev = newnode;
//}
//void LTPopBack(LTNode* phead) {
//	assert(phead);
//	assert(phead->next != phead);
//	LTNode* tail = phead->prev;
//	LTNode* tailPrev = tail->prev;
//	tailPrev->next = phead;
//	phead->prev = tailPrev;
//	free(tail);
//}
//void LTPushFront(LTNode* phead, LTDataType x) {
//	assert(phead);
//	LTNode* newnode = BuyLTNode(x);
//	newnode->next = phead->next;
//	phead->next->prev = newnode;
//	phead->next = newnode;
//	newnode->prev = phead;
//}
//void LTPopFront(LTNode* phead) {
//	assert(phead);
//	assert(phead->next != phead);
//	LTNode* first = phead->next;
//	LTNode* second = first->next;
//	free(first);
//	phead->next = second;
//	second->prev = phead;
//}

你可能感兴趣的:(笔记,数据结构,链表)