数据结构入门之复杂度,顺序表,链表,栈和队列(有代码)


什么是数据结构?说白了就是数据的排序的排布方式或者是管理方式,而今天所谈到的几种数据结构皆为 线性结构它们就好像一条线穿插起来,无论是物理地址意义上的线性结构还是逻辑意义上的线性结构都被称为线性数据结构

时间复杂度和空间复杂度

简单提一下这两个的概念,我们一般如何评价一个算法的好坏呢?我们从时间和所占额外开辟的空间大小来计算(记住这里的额外开辟空间的大小),从这里我们也就能看出一个算法的好坏通常用时间复杂度和空间复杂度来判断。
时间复杂度和空间复杂度我们都以最坏的情况去思考并且用大O的渐进表示方式

时间复杂度

时间复杂度:就是该程序的基本操作的执行次数,例如我们写了一个循环的嵌套
这里的N为未知变量

int count = 0;
//第一部分
 for (int i = 0; i < N ; ++ i)
 {
    for (int j = 0; j < N ; ++ j)
    {
        ++count;
    }
 }
 //第二部分
 for (int k = 0; k < 2 * N ; ++ k)
 {
    ++count;
 }
   

我们可以看见程序运行起来在循环嵌套的部分一共执行了NN次
而在第二部分我们执行了N
2次
所以我们整体运行的次数为N^ 2+2N次但是我们的时间复杂度并不是这里的 N^2+2N 而是O(N ^2)这里我们就需要讲一下:

大O的渐进表示法

大O的渐进表示法有三条定则
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
我们再来谈谈空间复杂度:

空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中额外临时占用存储空间大小的量度 。
记住这里的核心是额外临时占用存储空间大小,比如我们的某一个道题需要我们逆置一个数组,我们采用双指针的方式和我们采用一个新的数组去从后往前遍历存储所占的额外空间不同,第一种我们采用双指针的形式我们的空间复杂度为O(1),第二种采用额外数组的情况空间复杂度为O(N)这里为什么是N是因为我们以最坏的情况去想原数组的大小为N而新开辟数组的大小需要和原数组相同所以我们的空间复杂度是O(N),我们不会去计算一开始我们的那个数组所占的大小因为这个算法肯定需要一个数组,我们不会将算法中必要开辟的空间去计入在我们的空间复杂度当中,所以这就是双指针为什么是O(1)我们所计算的都是额外开辟的临时空间。

线性表(前言)

线性表
(linear list)
是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理地址上存储时,通常以数组的形式存储。
在逻辑上连续存储时,通常以链表的形式存储。

一.顺序表

数组就是最常见的顺序表,我们给定一个数组,它的每个元素的地址都是连续的也就是我们常说的物理地址上的连续存储。既然是数据结构就是对数据的各种增删查改,这也就是我们数据结构的核心和需要实现的内容。

这里就能看出我们的物理地址是否是连续存储的

1.1静态顺序表

对于一个静态的顺序表而言就是我们已经设定好这个顺序表的大小,超过了就直接报错,不存在动态增容的问题。
还是和以前一样我们封装成三个文件分别对应声明,实现和测试文件:
在这里插入图片描述

所以我们需要实现的接口如下:
这里作为我们的.h声明文件

#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<stdbool.h>
#include<stdlib.h>

#define capacity 10
//这里为什么要类型重定义呢?
//是因为我们的数据不见得每一次都是整形,我们可能下一次要存储浮点型等类型的数据,我们整改起来非常麻烦,
//所以采用类型重定义来解决这个问题,这样每次修改我们的数据我们只需要将这里的int改成我们需要的数据类型就好。
//而且这种方法在下面都会用到!!!
typedef int SLDataType;

typedef struct SeqList
{
	SLDataType data[capacity];
	int size;
}SL;

//初始化顺序表
void SLInit(SL* ps);

//头部插入数据
void SLPushFront(SL* ps, SLDataType x);

//尾部插入数据
void SLPushBack(SL* ps, SLDataType x);

//头部删除数据
void SLPopFront(SL* ps);

//尾部删除数据
void SLPopBack(SL* ps);

//查找数据
int SLSearch(SL* ps, SLDataType x);

//修改数据
void SLModify(SL* ps, int pos, SLDataType x);

//显示顺序表
void SLprint(SL* ps);

//判断顺序表是否是满的
bool SLFull(SL* ps);

//判断顺序表是否是空的
bool SLEmpty(SL* ps);

每一个接口的实现如下:
对于头插和尾插的逻辑实现不同,我们利用的是部分的挪动覆盖

#include"SeqList.h"

//初始化顺序表
void SLInit(SL* ps)
{
	memset(ps->data, 0, sizeof(ps->data));
	ps->size = 0;
}

//尾部插入数据
void SLPushBack(SL* ps, SLDataType x)
{
	if (SLFull(ps))
	{
		ps->data[ps->size] = x;
		ps->size++;
	}
	else
	{
		exit(-1);
	}
}

//尾部删除数据
void SLPopBack(SL* ps)
{
	assert(ps);
	if (SLEmpty(ps))
	{
		ps->data[ps->size - 1] = 0;
		ps->size--;
	}
	else
		exit(-1);

}

//头部插入数据
void SLPushFront(SL* ps, SLDataType x)
{
	if (SLFull(ps))
	{
		//遍历覆盖
		int i = 0;
		for (i = ps->size; i >= 0; i--)
		{
			ps->data[i + 1] = ps->data[i];
		}
		ps->data[0] = x;
		ps->size++;
	}
	else
	{
		exit(-1);
	}
}

//头部删除数据
void SLPopFront(SL* ps)
{
	assert(ps);
	if (SLEmpty(ps))
	{
		int i = 0;
		for (i = 1; i < ps->size; i++)
		{
			ps->data[i - 1] = ps->data[i];
		}
		ps->size--;
	}
	else
		exit(-1);
}

//显示顺序表
void SLprint(SL* ps)
{
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->data[i]);
	}
}

//查找数据
int SLSearch(SL* ps, SLDataType x)
{
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		if (ps->data[i] == x)
		{
			return i;
		}
	}
	return -1;
}

//修改数据
void SLModify(SL* ps, int pos, SLDataType x)
{
	ps->data[pos] = x;
}

//判断顺序表是否是满的
bool SLFull(SL* ps)
{
	if (ps->size == capacity)
	{
		return false;
	}
	else
	{
		return true;
	}
}

//判断顺序表是否是空的
bool SLEmpty(SL* ps)
{
	if (ps->size == 0)
	{
		return false;
	}
	else
	{
		return true;
	}
}

静态的顺序表实现起来和之前的通讯录还是有一些相似的。

1.2 动态顺序表

情景假设,如果我们需要设定一百个位置去存放我们的数据,但是现在有两百个数据需要我们去存储,那我们之前的静态顺序表是否就不够用了呢?那我们总不能再去修改底层的逻辑吧,那样的话是否太麻烦了呢?所以我们做了一种新的数据结构,在静态顺序表的基础上,我们让其能够自然增长也就是当数据满了的时候它会自动的去扩容这个扩容就是我们动态顺序表的核心
动态顺序表我们就不在栈上寻找空间了,我们选择在堆区上申请空间,并且用一个指针去维护我们的顺序表,这里用到了,malloc,realloc,calloc这三个函数都是我们在堆区上申请空间所需要的函数点击这里可以在目录中看到这三个函数的实现和粗略讲解详细的可以点击这里是Cplusplus的官方网址
那我们就开始实现我们的动态顺序表:
和静态顺序表一样我们需要三个文件封装,分别存有我们的声明,实现和测试三个文件。
我们需要声明的函数需要实现的功能分别为:

#define DefaultCapacity 4
#define AddCapacity 2

typedef int SLDateType;
typedef struct SeqList
{
	SLDateType* a;//顺序表中的数据由a来维护
	int size;     //有效数据的个数
	int capacity; //整个数据的容量
}SeqList;

// 对数据的管理:增删查改 
void SeqListInit(SeqList* ps);//初始化数据
void SeqListDestroy(SeqList* ps);//销毁数据

void SeqListPrint(SeqList* ps);//打印数据
void SeqListPushBack(SeqList* ps, SLDateType x);//尾插
void SeqListPushFront(SeqList* ps, SLDateType x);//头插
void SeqListPopFront(SeqList* ps);//头删
void SeqListPopBack(SeqList* ps);//尾删

// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);

每个函数对应的实现方式如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"

//初始化
void SeqListInit(SeqList* ps)
{
	assert(ps);
	ps->a = (SLDateType*)malloc(sizeof(SLDateType) * DefaultCapacity);
	if (ps->a == NULL)
	{
		perror("malloc filled");
		exit(-1);
	}
	ps->size = 0;
	ps->capacity = DefaultCapacity;
}

//销毁数据
void SeqListDestroy(SeqList* ps)
{
	free(ps->a);
	ps->a = NULL;
	ps->size = 0;
	ps->capacity = DefaultCapacity;
}

//检查容量函数
void Capacity_Check(SeqList* ps)
{
	if (ps->capacity == ps->size)
	{
		//SLDateType* tmp = (SLDateType*)realloc(ps->a, (sizeof(SLDateType) * DefaultCapacity) + AddCapacity * (sizeof(SLDateType)));//这里的空间被写死了实现不了动态增容
		SLDateType* tmp = (SLDateType*)realloc(ps->a, (ps->capacity * AddCapacity * (sizeof(SLDateType))));
		if (tmp == NULL)
		{
			perror("realloc filled");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity += AddCapacity;
	}
}

//尾部插入数据
void SeqListPushBack(SeqList* ps, SLDateType x)
{
	Capacity_Check(ps);
	ps->a[ps->size] = x;
	ps->size++;
}

//打印数据函数
void SeqListPrint(SeqList* ps)
{
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
}

//尾部删除数据
void SeqListPopBack(SeqList* ps, SLDateType x)
{
	//不加这个判断会导致我们如果一直使用PopBack函数,会导致我们后续的插入会有问题,因为VS存在越界检查区,
	//你只是越界访问几个字节的话它是不会报错的但是实际上代码是有问题的
	if (ps->size > 0)
		ps->size--;
}

//头部插入函数
void SeqListPushFront(SeqList* ps, SLDateType x)
{
	Capacity_Check(ps);
	int i = 0;//控制循环
	for (i = ps->size; i >= 0; i--)
	{
		ps->a[i + 1] = ps->a[i];
	}
	ps->a[0] = x;
	ps->size++;
}

//头部删除函数
void SeqListPopFront(SeqList* ps)
{
	assert(ps->size > 0);
	int i = 0;
	for (i = 1; i < ps->size; i++)
	{
		ps->a[i - 1] = ps->a[i];
	}
	ps->size--;
}

//顺序表查找函数 返回其下标
int SeqListFind(SeqList* ps, SLDateType x)
{
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	exit(-1);
}

// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDateType x)
{
	//判断是否超出范围
	if (pos< 0 || pos >ps->size)
	{
		exit(-1);//也可以用assert		
	}

	int i = 0;
	Capacity_Check(ps);
	for (i = ps->size - 1; i >= pos - 1; i--)
	{
		ps->a[i + 1] = ps->a[i];
	}
	ps->a[pos - 1] = x;

	ps->size++;

}

// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos)
{
	assert(pos >= 0 && pos < ps->size);
	int i = pos;
	for (i = pos; i < ps->size; i++)
	{
		ps->a[i - 1] = ps->a[i];
	}
	ps->size--;
}

这里的尾删还是有一些需要注意的地方的,我在注释上已标明出来了。

顺序表的问题及思考

问题:

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
    这里存在的拷贝数据,是因为realloc函数当我们的空间不足以增加到你自己设定的大小之后它会自动的找一个新的地方去存储我们的数据把之前的数据全部都拷贝到这个新的地址上,所以存在申请数据拷贝数据的损耗
  3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
    200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

思考:如何解决以上问题呢?下面给出了链表的结构来看看。

二.链表

链表和顺序表一样都为线性结构,但是链表的实现方式是通过逻辑上连续,物理地址上并不一定连续,这就和我们的顺序表有很大差别了,我们的顺序表都是紧挨着存储的每个元素的物理地址都是连续的,那就让我们看看它是怎么实现的吧,这里用到了动态内存开辟,所以不了解的还是先去看看我之前写的通讯录的改造利用到了动态内存开辟C通讯录和动态内存开辟链表的实现逻辑是利用到了指针的链接来实现的。
我们的链表一共有八种实现方式我们只取最经典的单链表和结构最完美的双向带头循环链表,
这里的八种怎么来的呢?
在这里插入图片描述
说白了就是两两结合就一共有八种

2.1单链表

这是单项不带头不循环链表,也是最容易出题和最经典的链表
我们链表是通过一个个内存块的指向链接起来的,它和火车一样,一节一节的我们用一个链子把他们拴在一起,这里的每一节就代表我们的数据我们称其为节点,而这里的链子就是我们的指针我们就将其分划成为了数据域和指针域。
在这里插入图片描述
我们先来看一下是如何在一辆火车中去对应的,每个标红的矩形都是我们的数据区也就是节点,而每一个与下一节火车链接的地方我们称之为指针,指向的是我们的下一个节点,我们需要用指针去链接我们的每一个节点。
![在这里插入图片描述](https://img-blog.csdnimg.cn/38bed9eda96b487fa149cc74315d003f.png在这里插入图片描述

像这样链接起来,我们会发现我们的最后一个节点没有东西去与我们链接,它链接了空气,我们就让其链接的地方为空指针NULL,为什么不能直接不写呢?因为我们不能让我们的指针是无指定方向的,不然它可能会成为一个野指针而我们对野指针进行使用的时候风险是非常大的。
我们需要实现的目的和顺序表一样都是为了数据的增删改查:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SListdatatype;

typedef struct SListNode
{
	SListdatatype data;//数据

	struct SListNode* next;//节点指向
}SListNode;


//链表的打印函数
void SListprintf(SListNode* phead);

//创造新节点的函数
SListNode* SListBuyNode(SListdatatype x);

//头部插入函数
void SLTPushFront(SListNode** phead, SListdatatype x);

//尾插
void SLTPushBack(SListNode** phead, SListdatatype x);

//尾删
void SLTPopBack(SListNode** phead);

//头删
void SLTPopFront(SListNode** phead);

//寻找pos的位置
SListNode* SLTFindPos(SListNode* phead, SListdatatype val);

//在pos位置之前插入
void SLTinsetpos(SListNode** phead, SListNode* pos, SListdatatype val);

//在pos位置之后插入
void SLTinsetposAfter(SListNode** phead, SListNode* pos, SListdatatype val);

//删除pos位置的值
void SLTPopPos(SListNode** phead, SListNode* pos);

每一个接口的实现方式如下:

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

//打印链表函数的实现
void SListprintf(SListNode* phead)
{
	assert(phead);

	SListNode* cur = phead;

	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}

	printf("NULL\n");

}

//创造一个新的节点
SListNode* SListBuyNode(SListdatatype x)
{
	SListNode* NewNode = (SListNode*)malloc(sizeof(SListNode));
	if (NewNode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	NewNode->data = x;
	NewNode->next = NULL;

	return NewNode;
}

//头部插入函数
void SLTPushFront(SListNode** phead, SListdatatype x)
{
	SListNode* NewNode = SListBuyNode(x);
	NewNode->next = *phead;
	*phead = NewNode;
}

//尾插
void SLTPushBack(SListNode** phead, SListdatatype x)
{
	SListNode* NewNode = SListBuyNode(x);
	SListNode* cur = *phead;
	if (*phead == NULL)
	{
		*phead = NewNode;
	}
	else
	{
		while (cur->next != NULL)
		{
			cur = cur->next;
		}
		cur->next = NewNode;
	}

}

//尾删
void SLTPopBack(SListNode** phead)
{
	assert(*phead);
	SListNode* cur = *phead;
	if ((*phead)->next == NULL)
	{
		free(*phead);
		*phead = NULL;
	}
	else
	{
		while (cur->next->next != NULL)
		{
			cur = cur->next;
		}
		free(cur->next);
		cur->next = NULL;
	}
}

//头删
void SLTPopFront(SListNode** phead)
{
	assert(*phead);

	SListNode* tmp = (*phead)->next;
	free(*phead);
	*phead = tmp;
}


//寻找pos的位置
SListNode* SLTFindPos(SListNode* phead, SListdatatype val)
{
	assert(phead);
	SListNode* cur = phead;
	while (cur)
	{
		if (cur->data == val)
		{
			return cur;
		}
		cur = cur->next;
	}
	exit(-1);
}

void SLTinsetpos(SListNode** phead, SListNode* pos, SListdatatype val)
{
	assert(*phead);

	SListNode* NewNode = SListBuyNode(val);

	SListNode* posfront = *phead;
	if (*phead == pos)
	{
		SLTPushFront(phead, val);
	}
	else
	{
		while (posfront->next != pos)
		{
			posfront = posfront->next;
		}
		posfront->next = NewNode;
		NewNode->next = pos;
	}
}

void SLTinsetposAfter(SListNode** phead, SListNode* pos, SListdatatype val)
{
	assert(*phead);

	SListNode* cur = *phead;

	SListNode* NewNode = SListBuyNode(val);

	//if (*phead == NULL)
	//{
	//	SLTPushBack(phead, val);
	//}

	if (pos->next == NULL)
	{
		SLTPushBack(phead, val);
	}
	else
	{
		NewNode->next = pos->next;
		pos->next = NewNode;
	}
}

void SLTPopPos(SListNode** phead, SListNode* pos)
{
	assert(*phead);

	SListNode* posfront = *phead;

	while (posfront->next != pos)
	{
		posfront = posfront->next;
	}

	if (pos == *phead)
	{
		SLTPopFront(phead);
	}
	else if (posfront->next == NULL)
	{
		SLTPopBack(phead);
	}
	else
		posfront->next = pos->next;

}

这里非常麻烦的一点就是尾插或者是尾删,因为我们都需要去遍历一遍我们的链表才能找到尾部,但是顺序表麻烦的是头插头删,因为它需要挪动数据,这我们就能看出来一丝差异了,这个链表的优势是什么呢?它不同于我们的动态增容的顺序表,它是我们需要一节车厢(一个节点)我们就去堆上malloc一个节点出来,避免了空间的浪费而且对于头部的改变效率也高。

2.2双向带头循环链表

双向带头循环链表是链表中结构比较复杂但是功能性最强的一种链表,什么叫做双向带头循环?
![在这里插入图片描述](https://img-blog.csdnimg.cn/22e56be7b9a640abbdb2a8d6dc124782.png![在这里插入图片描述](https://img-blog.csdnimg.cn/52d744bec5bd4b2494b706480c0a7165.png在这里插入图片描述

大致的模型如此画的丑了点儿,我们一开始就先malloc一块儿空间这块空间我们不需要去存放数据,只是将head和tail这两个头尾指针指向这里,当我们有数据的时候我们用next去指向这个新的数据,然后以此类推增加数据
这里的优点我们能不能看出来呢?
与单链表相比
1.单链表用到了二级指针但是我们带个头的话(也称哨兵位,就是不存数据单纯的记录在此)就不需要我们使用二级指针
2.方便我们找头尾头的位置就是tail的next,而尾巴就是head的prev就不需要我们每一次都去遍历我们的链表。
3.不需要特殊处理第一次尾插节点
函数的声明:

在这里插入代码片#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//实现双向带头循环的链表的增删查改
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

//初始化双向链表
ListNode* ListInit(ListNode* pHead);

// 创建节点
ListNode* BuyListNode(LTDataType x);

// 双向链表销毁
void ListDestory(ListNode* pHead);

// 双向链表打印
void ListPrint(ListNode* pHead);

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);

// 双向链表尾删
void ListPopBack(ListNode* pHead);

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);

// 双向链表头删
void ListPopFront(ListNode* pHead);

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

接口的实现:

#define _CRT_SECURE_NO_WARNINGS 1
#include"List.h"

//初始化双向链表
ListNode* ListInit(ListNode* pHead)
{
	ListNode* NewNode = BuyListNode(-1);
	pHead = NewNode;
	pHead->next = pHead;
	pHead->prev = pHead;
	return pHead;
}


// 创建结点.
ListNode* BuyListNode(LTDataType x)
{
	ListNode* NewNode = (ListNode*)malloc(sizeof(ListNode));
	if (NewNode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	NewNode->data = x;
	NewNode->next = NULL;
	NewNode->prev = NULL;

	return NewNode;
}

//双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	/*ListNode* NewNode = BuyListNode(x);
	ListNode* pHeadprev = pHead->prev;
	NewNode->next = pHead;
	pHead->prev = NewNode;
	NewNode->prev = pHeadprev;
	pHeadprev->next = NewNode;*/
	ListInsert(pHead, x);
}

// 双向链表打印
void ListPrint(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	printf("pHead<=>");
	while (cur != pHead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("pHead\n");
}

//双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	/*ListNode* pHeadnext = pHead->next;
	ListNode* NewNode = BuyListNode(x);

	pHead->next = NewNode;
	NewNode->next = pHeadnext;
	pHeadnext->prev = NewNode;
	NewNode->prev = pHead;*/

	ListInsert(pHead->next, x);
}

// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->prev);

	/*ListNode* del = pHead->prev;
	ListNode* newtail = pHead->prev->prev;

	free(del);
	pHead->prev = newtail;
	newtail->next = pHead;*/
	ListErase(pHead->prev);
}

//双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead);
	assert(pHead->next);
	/*ListNode* del = pHead->next;
	ListNode* newhead = pHead->next->next;

	free(del);
	pHead->next = newhead;
	newhead->prev = pHead;*/
	ListErase(pHead->next);
}

// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x)
{
	assert(pHead);
	ListNode* cur = pHead->next;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}

	return NULL;
}

// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x)
{
	ListNode* posprev = pos->prev;
	ListNode* NewNode = BuyListNode(x);

	posprev->next = NewNode;
	NewNode->prev = posprev;
	NewNode->next = pos;
	pos->prev = NewNode;
}

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	ListNode* posprev = pos->prev;
	ListNode* posnext = pos->next;
	free(pos);

	posprev->next = posnext;
	posnext->prev = posprev;
}

// 双向链表销毁
void ListDestory(ListNode* pHead)
{
	ListNode* cur = pHead->next;
	ListNode* curnext = cur->next;
	while (cur != pHead)
	{
		free(cur);
		cur = curnext;
		pHead->next = curnext;
		curnext = curnext->next;
	}
}

我们可以发现这个在我们实现双向链表的时候我们的在pos处的插入函数我们进行了复用,这是因为我们如果想在第一个节点之前插入我们就和头插的效果是一样的,在哨兵位的头前面一个节点后插入或者就在哨兵位节点的前面插入我们就和尾插的效果是一样的,所以如果将来被考到如何迅速完成一个双向带头循环链表的话我们就可以使用复用在pos位置之前的位置插入节点的方式来迅速完成这个链表。

三顺序表与链表的区别和优劣势

这里的链表我们指的是最好的带头双向循环链表

在这里插入图片描述
这里的缓存利用率是如何比较的呢?我们的CPU一般是在缓存和寄存器的空间中去寻找数据的,因为我们的内存跟不上我们CPU的处理数据的速度,只有寄存器和缓存可以跟得上,所以CPU一般先把在内存上的我们需要的数据加载到我们的缓存和寄存器中再去寻找需要的数据,但是不会是我们需要一个字节大小的数据它CPU就只加载一个字节的数据到我们的缓存中吧,这样每次都要去加载会很麻烦,所以我们的CPU就一次性把我们几十个字节或者一百多个字节(由电脑配置决定)全部加载到我们的缓存中,但是呢我们说过顺序表的地址是连续存储的但是我们的链表它只是在逻辑上连续它并不是在物理空间上连续存储导致我们在缓存中被一次性加载的数据可能没有把我们链表的所有数据都拿到,但是顺序表不一样,它是连续存储的,我们在进行加载的时候可能会把后面的物理地址连续的数据全部加载到我们的缓存中,这样我们只用加载几次就能拿到我们顺序表中的所有的数据但是链表需要的次数可能就更多了,导致我们CPU加载的时间过长缓存的利用率低下,我们的顺序表缓存利用率就高一些这是由计算机的底层实现原理决定的。 想深入了解的可以点击这里与程序员相关的CPU缓存知识

四.栈

什么叫做栈?我们可能只听说过在内存中的栈区,这里的栈是一种特殊的线性数据结构,它和在内存中的栈的存储方式一样,它是只允许在一端进行插入删除且我们要满足“先入后出的概念“ 入数据的时候我们称为入栈,出数据的时候我们称为出栈,栈的两端我们称之为栈顶和栈底,但是进行操作的时候我们只针对 栈顶。
比如我们入两个数据一个是8,一个是2
在这里插入图片描述
删除的时候也是从栈顶开始删除
在这里插入图片描述
实现栈的方式有两种一种叫做数组栈一种叫做链式栈,顾名思义数组栈就是利用数组来实现一个栈,链式栈就是利用链表去实现一个栈,但是我们刚刚看图可以发现栈的每一次操作都是在栈顶的,对于一组数来说就是在尾部进行操作,比如说:
1,2,3,4,5,依次插入这五个数,我们要实现先进入的后出,后进入的先出,也就是通过尾插,尾删来控制我们的栈。所以我们选择用数组的方式来实现我们的栈,因为尾部操作对于我们的链表来说需要进行找尾要遍历链表效率低下,如果我们使用双向带头循环的话也是可以实现的但是双向带头循环链表的结构稍微比顺序表难写一些因为它有两个指针而且还得防止卡在循环之中,所以我们还是选择数组来实现我们的栈
注意这里的top我们设置的是0,在数组中下标为0代表着第一个元素所以在这里我们的top指的是当前元素的下一个所以我们在使用STTOP这个接口的时候我们才会将我们的Top进行减一,而且我们这里使用的是动态增长的顺序表来实现我们的栈。
栈各种接口的声明:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

#define DefaultCapacity 4

typedef int STdatatype;
typedef struct Stack
{
	STdatatype* a;
	int top;
	int capacity;
}ST;

//栈的初始化
void STInit(ST* ps);

//栈的销毁
void Destory(ST* ps);

//栈的插入
void STPush(ST* ps, STdatatype x);

//栈的删除
void STPop(ST* ps);

//栈的大小
int STSize(ST* ps);

//栈顶元素的显现
STdatatype STTop(ST* ps);

//判断栈是否为空
bool STEmpty(ST* ps);

接口的定义:

#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"

//栈的初始化
void STInit(ST* ps)
{
	assert(ps);

	ps->a = NULL;
	ps->capacity = 0;
	ps->top = 0;
}

//栈的销毁
void Destory(ST* ps)
{
	assert(ps);

	free(ps->a);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = 0;
}

//栈的插入
void STPush(ST* ps, STdatatype x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int NewCapacity = ps->capacity == 0 ? DefaultCapacity : (ps->capacity) * 2;
		STdatatype* tmp = ps->a;
		tmp = (STdatatype*)realloc(ps->a, sizeof(STdatatype) * NewCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}
		ps->a = tmp;
		ps->capacity = NewCapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

//栈的删除元素
void STPop(ST* ps)
{
	assert(ps);

	assert(ps->top > 0);

	--ps->top;
}

//栈的尺寸
int STSize(ST* ps)
{
	assert(ps);

	return ps->top;

}

//栈顶元素的显现
STdatatype STTop(ST* ps)
{
	assert(ps);

	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}

//判断栈是否为空
bool STEmpty(ST* ps)
{
	assert(ps);

	return ps->top == 0;
}

当我们实现了顺序表的静态和动态的时候我们会发现栈和它长得非常类似,实现起来也就非常好实现了,它也没有在任意位置上的插入删除,整体就更简洁了在某些题型上会展现其优势。

五.队列

队列和栈是反过来的,我们刚说的栈是先入后出,我们的队列则是先入先出,那我们仍然以1,2,3,4,5这一组数据来看,我们要做到先入先出是不是就是要把这里的1给Pop掉也就是删除掉,这就说明了我们是在头部进行数据的增删,而我们这里仍然有两种实现方式一种是数组实现的队列一种是链表实现的队列,我们要在头部进行数据的处理,那数组的效率每次都要挪动数据就很麻烦我们就采用链表的方式去实现队列
在这里插入图片描述

接口的声明:
我们这里为什么要用到两个结构体呢?我们在这里实现了队头和队尾元素的显现,而且我们这里和单链表一样需要修改我们的头,所以如果我们只有一个结构体的话就是把头尾全部放在一个结构体里去声明,那么我们会用到二级指针或者利用返回值去解决,但是我们这里有两个指针我们的结构体只能返回一个指针和单链表的结构差不多我们为此去在创造一个结构体,这个结构体就包含了头尾的指针。

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

typedef int QDataType;

//每个节点的结构
typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}Qnode;

//整个队列链表的头尾节点
typedef struct Queue
{
	Qnode* front;
	Qnode* rear;
	int size;
}Queue;

// 初始化队列 
void QueueInit(Queue* q);

// 销毁队列 
void QueueDestroy(Queue* q);

// 尾插
void QueuePush(Queue* q, QDataType data);

// 头删
void QueuePop(Queue* q);

// 获取队列头部元素 
QDataType QueueFront(Queue* q);

// 获取队列队尾元素 
QDataType QueueBack(Queue* q);

// 获取队列中有效元素个数 
int QueueSize(Queue* q);

// 检测队列是否为空,如果为空返回0 
int QueueEmpty(Queue* q);

队列接口的定义:

#define _CRT_SECURE_NO_WARNINGS 1
#include"Queue.h"

// 初始化队列 
void QueueInit(Queue* q)
{
	assert(q);

	q->front = NULL;
	q->rear = NULL;
	q->size = 0;
}

// 销毁队列 
void QueueDestroy(Queue* q)
{
	assert(q);

	Queue* cur = q;

	//遍历销毁
	while (cur->front)
	{
		Qnode* next = cur->front->next;
		free(cur->front);
		cur->front = next;
	}
	cur->rear = NULL;
	cur->size = 0;
}

// 尾插
void QueuePush(Queue* q, QDataType data)
{
	assert(q);
	Queue* cur = q;

	//制造节点	
	Qnode* NewNode = (Qnode*)malloc(sizeof(Qnode));
	if (NewNode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	NewNode->data = data;
	NewNode->next = NULL;
	//先判断 因为我们这个队列是无哨兵位的头的链表所以第一次判断如果是空指针则赋值
	if (cur->rear == NULL)
	{
		cur->front = cur->rear = NewNode;
	}
	else
	{
		cur->rear->next = NewNode;
		cur->rear = NewNode;
	}

	cur->size++;
}

// 头删
void QueuePop(Queue* q)
{
	assert(q);
	assert(q->front);

	Qnode* next = q->front->next;
	free(q->front);

	q->front = next;
	if (next == NULL)
		q->rear = NULL;

	q->size--;
}

// 获取队列头部元素 
QDataType QueueFront(Queue* q)
{
	assert(q);
	assert(q->front);

	return q->front->data;
}

// 获取队列队尾元素 
QDataType QueueBack(Queue* q)
{
	assert(q);
	assert(q->rear);

	return q->rear->data;
}

// 获取队列中有效元素个数 
int QueueSize(Queue* q)
{
	assert(q);

	return q->size;
}

// 检测队列是否为空,如果为空返回真,如果非空返回假 
int QueueEmpty(Queue* q)
{
	assert(q);

	return !(q->size == 0);
}

这上面的测试文件我都没有写,大家可以自己去尝试写测试文件。
所有的代码我都放在gitee里了,有兴趣的话可以一看。
我的gitee链接
如果有写错的地方欢迎大家来指出,感谢支持!!
也希望各位好好努力!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
队列(Queue)是一种先进先出(First-In, First-Out, FIFO)的数据结构。在队列中,只允许在一端进行插入操作,而在另一端进行删除操作。添加元素的操作称为入队(enqueue),删除元素的操作称为出队(dequeue)。 栈(Stack)是一种后进先出(Last-In, First-Out, LIFO)的数据结构。在栈中,只允许在一端进行插入和删除操作。添加元素的操作称为入栈(push),删除元素的操作称为出栈(pop)。 链表(Linked List)是一种非连续的、非顺序的数据结构链表中的数据元素通过链来进行连接。各个元素(节点)包含了存储数据的内容以及指向下一个元素的指针。链表可以分为单向链表和双向链表两种类型。 线性表(List)是数据元素按照一定顺序排列的数据结构。线性表中的元素可以是相同类型的,也可以是不同类型的。线性表的特性包括元素的有序性、位置的固定性以及元素的可重复性。线性表可以通过数组或链表来实现。 排序(Sorting)是对一组数据元素进行按照一定规则重新排列的操作。排序的目的是为了使数据具备一定的有序性。常见的排序算法包括冒泡排序、插入排序、选择排序、快速排序、归并排序等。排序算法的选择取决于数据量的大小、排序的稳定性要求以及时间和空间复杂度的限制。 总结起来,队列和栈是两种基本的数据结构链表和线性表是数据元素排列的方式,排序是一种对元素进行排列的操作。理解这些知识点可以帮助我们更好地理解和应用Java的数据结构和算法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老幺*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值