【数据结构】顺序表和链表详解顺序表和链表的实现

news2024/11/23 1:09:14

主页:醋溜马桶圈-CSDN博客

专栏:数据结构_醋溜马桶圈的博客-CSDN博客

gitee:mnxcc (mnxcc) - Gitee.com

目录

1.线性表

1.1 顺序表

1.1.1 概念及结构

1.1.2 静态顺序表

1.1.3 动态顺序表

1.2 链表

1.2.1 链表的概念及结构

1.2.2 链表的分类 

1.2.2.1 单向或者双向

1.2.2.2 带头或者不带头

1.2.2.3 循环或者非循环

1.3 顺序表和链表的区别

2.顺序表的实现

2.1 创建顺序表

2.2 基本的增删查改接口

2.2.1 顺序表初始化

2.2.2 顺序表的销毁

2.2.3 检查顺序表的容量

2.2.4 打印顺序表 

2.2.5 顺序表的尾插

2.2.6 顺序表的头插

2.2.7 顺序表的尾删

2.2.8 顺序表的头删

2.2.9 任意位置的插入

2.2.10 任意位置的删除

2.2.11 顺序表的查找

2.3实现代码

2.3.1 SeqList.h

2.3.2 SeqList.c

3.单链表的实现

3.1 认识单链表

3.2 创建单链表

3.3 单链表的操作

3.3.1 打印单链表

3.3.2 开辟新空间

3.3.3 尾插

3.3.4 头插

3.3.5 尾删

3.3.6 头删

3.3.7 查找

3.3.8 在pos前面插入

3.3.9 删除pos位置

3.3.10 销毁

3.3.11 在pos位置之后插入

3.3.12 删除pos位置之后的值

3.4 实现代码

3.4.1 SList.h

3.4.2 SList.c

4.带头双向循环链表的实现

4.1 认识带头双向循环链表

4.1.1 双向链表

4.1.2 循环链表

4.1.3 带头链表

4.1.4 带头双向循环链表

4.1.5 双向链表的优势和不足

4.1.6 顺序表的优势和不足

4.2 实现带头双向循环链表

4.2.1 创建带头双向循环链表

4.2.2 初始化

4.2.3 创建返回链表的头结点

4.2.4 打印链表

4.2.5 尾插

4.2.6 尾删

4.2.7 头插

4.2.8 头删

4.2.9 查找

4.2.10 在pos位置前插入

4.2.11 删除pos位置

4.2.12 销毁

4.3 实现代码

4.3.1 ListNode.h

4.3.2 ListNode.c

5.数组下标为什么从0开始

5.1 原因

5.2 拓展

6.环型链表

6.1 环型链表是什么

6.2 快慢指针判断环形链表

6.2.1 思考

6.2.2 推导思路

6.2.3 结论


1.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串..

线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储:

1.1 顺序表

1.1.1 概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构

一般情况下采用数组存储,在数组上完成数据的增删查改

顺序表一般可以分为:

1.1.2 静态顺序表

静态顺序表:使用定长数组存储元素



1.1.3 动态顺序表

动态顺序表:使用动态开辟的数组存储

1.2 链表

1.2.1 链表的概念及结构

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

现实中

数据结构中

注意:

  1. 从上图可以看出,链式结构在逻辑上是连续的,但在物理上不一定连续
  2. 现实中的结点一般都是从堆上申请出来的 
  3. 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

1.2.2 链表的分类 

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

1.2.2.1 单向或者双向

1.2.2.2 带头或者不带头

1.2.2.3 循环或者非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构: 

无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。

带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向 循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

1.3 顺序表和链表的区别

与程序员相关的CPU缓存知识 | 酷 壳 - CoolShell

2.顺序表的实现

2.1 创建顺序表

2.2 基本的增删查改接口

2.2.1 顺序表初始化

顺序表的初始化我们只需要讲指针置为空指针
然后将当前数据元素个数和最大数据元素个数置为0
到插入时我们便会动态开辟空间给指针a

//顺序表的初始化
void SLInit(SL* ps)
{
	ps->a = NULL;//置为空指针
	ps->size = 0;//数据个数为0
	ps->capacity = 0;//空间大小置为0
}

2.2.2 顺序表的销毁

//顺序表的销毁
void SLDestroy(SL* ps)
{
	if (ps->a != NULL)
	{
		free(ps->a);
		ps->a = NULL;
		ps->size = 0;
		ps->capacity = 0;
	}
}

2.2.3 检查顺序表的容量

//检查顺序表的容量
void SLCheckCapacity(SL* ps)
{
	if (ps->size == ps->capacity)
	{
		int newCapacity = ps->capacity = 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = realloc(ps->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newCapacity;
	}
}

2.2.4 打印顺序表 

//打印顺序表
void SLPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

2.2.5 顺序表的尾插

//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
	SLCheckCapacity(ps);
	ps->a[ps->size] = x;
	ps->size++;
}

2.2.6 顺序表的头插

//顺序表的头插
void SLPushFront(SL* ps, SLDataType x)
{
	SLCheckCapacity(ps);
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
	}
	ps->a[0] = x;
	ps->size++;
}

2.2.7 顺序表的尾删

//顺序表的尾删
void SLPopBack(SL* ps)
{
	assert(ps->size > 0);
	//ps->a[ps->size - 1] = -1;
	ps->size--;
}

2.2.8 顺序表的头删

//顺序表的头删
void SLPopFront(SL* ps)
{
	//for (int i = 0; i < ps->size; i++)
	//{
	//	ps->a[i] = ps->a[i + 1];
	//}
	//ps->size--;
	assert(ps);
	assert(ps->size > 0);
	int begin = 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}
	ps->size--;
}

2.2.9 任意位置的插入

//任意位置的插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
    SLCheckCapacity(ps);
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos] = x;
	ps->size++;
}

2.2.10 任意位置的删除

//任意位置的删除
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	int begin = pos;
	while (begin < ps->size)
	{
		ps->a[begin] = ps->a[begin+1];
		++begin;
	}
	ps->size--;
}

2.2.11 顺序表的查找

//顺序表的查找
//找到返回下标,找不到返回-1
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

2.3实现代码

2.3.1 SeqList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SLDataType;
//顺序表的动态存储
typedef struct SeqList
{
	SLDataType* a;     //指向动态开辟的数组
	int size;           //有效元素个数
	int capacity;       //容量空间的大小
}SL;

//顺序表的初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);

//检查顺序表的容量
void SLCheckCapacity(SL* ps);
//打印顺序表
void SLPrint(SL* ps);


//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x);
//顺序表的头插
void SLPushFront(SL* ps, SLDataType x);
//顺序表的尾删
void SLPopBack(SL* ps);
//顺序表的头删  
void SLPopFront(SL* ps);

//任意位置的插入
void SLInsert(SL* ps, int pos, SLDataType x);
//任意位置的删除
void SLErase(SL* ps, int pos);

//顺序表的查找
//找到返回下标,找不到返回-1
int SLFind(SL* ps, SLDataType x);

2.3.2 SeqList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"

//顺序表的初始化
void SLInit(SL* ps)
{
	ps->a = NULL;//置为空指针
	ps->size = 0;//数据个数为0
	ps->capacity = 0;//空间大小置为0
}
//顺序表的销毁
void SLDestroy(SL* ps)
{
	if (ps->a != NULL)
	{
		free(ps->a);
		ps->a = NULL;
		ps->size = 0;
		ps->capacity = 0;
	}
}
//检查顺序表的容量
void SLCheckCapacity(SL* ps)
{
	if (ps->size == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = realloc(ps->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newCapacity;
	}
}
//打印顺序表
void SLPrint(SL* ps)
{
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}
//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
	SLCheckCapacity(ps);
	ps->a[ps->size] = x;
	ps->size++;
}
//顺序表的头插
void SLPushFront(SL* ps, SLDataType x)
{
	SLCheckCapacity(ps);
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;
}
//顺序表的尾删
void SLPopBack(SL* ps)
{
	assert(ps->size > 0);
	//ps->a[ps->size - 1] = -1;
	ps->size--;
}
//顺序表的头删
void SLPopFront(SL* ps)
{
	//for (int i = 0; i < ps->size; i++)
	//{
	//	ps->a[i] = ps->a[i + 1];
	//}
	//ps->size--;
	assert(ps);
	assert(ps->size > 0);
	int begin = 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}
	ps->size--;
}
//任意位置的插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	SLCheckCapacity(ps);
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos] = x;
	ps->size++;
}
//任意位置的删除
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);
	int begin = pos;
	while (begin < ps->size)
	{
		ps->a[begin] = ps->a[begin+1];
		++begin;
	}
	ps->size--;
}
//顺序表的查找
//找到返回下标,找不到返回-1
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}
	return -1;
}

3.单链表的实现

顺序表存在下面的问题:

  • 尾插效率还不错,头插或中间插入删除,需要挪动数据,效率低
  • 满了以后只能扩容,扩容是有一定的消耗的,扩容一般存在一定的空间浪费

3.1 认识单链表

如果想要插入一个结点,就不需要挪动数据了,改指针的指向就可以了

同样我们删除结点,直接将前一个结点的指针指向后一个结点就可以了

首先我们还是从工程的角度去考虑,创建SList.h SList.c test.c三个文件

SList.h放置函数的声明

SList.c放置函数的定义

test.c进行测试 

3.2 创建单链表

3.3 单链表的操作

3.3.1 打印单链表

//打印单链表
//尽量不要动phead
void SLTPrint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}

3.3.2 开辟新空间

//开辟新空间
SLNode* CreateNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
}

3.3.3 尾插

//尾插
void SLTPushBack(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

3.3.4 头插

//头插
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

3.3.5 尾删

//尾删
void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* prev = NULL;
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}

3.3.6 头删

//头删
void SLTPopFront(SLNode** pphead)
{
	assert(*pphead)
		;
	SLNode* tmp = *pphead;
	*pphead = (*pphead)->next;
	free(tmp);
}

3.3.7 查找

//查找
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
	SLNode* cur = phead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}

3.3.8 在pos前面插入

//在pos前面插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	//assert((!pos && !(*pphead)) || (pos && (*pphead)));
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}

3.3.9 删除pos位置

//删除pos位置
void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

3.3.10 销毁

//销毁
void SLTDestroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* cur = *pphead;
	while (cur->next != NULL)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

3.3.11 在pos位置之后插入

// 单链表在pos位置之后插入x
void SLTInsertAfter(SLNode* pos, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

3.3.12 删除pos位置之后的值

// 单链表删除pos位置之后的值
void SLTEraseAfter(SLNode* pos)
{
	assert(pos->next != NULL);
	pos->next = pos->next->next;
	free(pos->next);
	pos->next = NULL;
}

3.4 实现代码

3.4.1 SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
//创建单链表
typedef int SLNDataType;
typedef struct SLNode
{
	SLNDataType val;
	struct SLNode* next;
}SLNode;

//打印单链表
//尽量不要动phead
void SLTPrint(SLNode* phead);
//开辟新空间
SLNode* CreateNode(SLNDataType x);

//尾插
void SLTPushBack(SLNode** pphead, SLNDataType x);
//头插
void SLTPushFront(SLNode** pphead, SLNDataType x);
//尾删
void SLTPopBack(SLNode** pphead);
//头删
void SLTPopFront(SLNode** pphead);

//查找
SLNode* SLTFind(SLNode* phead, SLNDataType x);
//在pos前面插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x);
//删除pos位置
void SLTErase(SLNode** pphead, SLNode* pos); 

// 单链表在pos位置之后插入x
void SLTInsertAfter(SLNode* pos, SLNDataType x);
// 单链表删除pos位置之后的值
void SLTEraseAfter(SLNode* pos);

//销毁
void SLTDestroy(SLNode** pphead);

3.4.2 SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//打印单链表
//尽量不要动phead
void SLTPrint(SLNode* phead)
{
	SLNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->val);
		cur = cur->next;
	}
	printf("NULL\n");
}
//开辟新空间
SLNode* CreateNode(SLNDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	return newnode;
}
//尾插
void SLTPushBack(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}
//头插
void SLTPushFront(SLNode** pphead, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}
//尾删
void SLTPopBack(SLNode** pphead)
{
	assert(*pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLNode* prev = NULL;
		SLNode* tail = *pphead;
		while (tail->next != NULL)
		{
			prev = tail;
			tail = tail->next;
		}
		free(tail);
		prev->next = NULL;
	}
}
//头删
void SLTPopFront(SLNode** pphead)
{
	assert(*pphead)
		;
	SLNode* tmp = *pphead;
	*pphead = (*pphead)->next;
	free(tmp);
}
//查找
SLNode* SLTFind(SLNode* phead, SLNDataType x)
{
	SLNode* cur = phead;
	while (cur)
	{
		if (cur->val == x)
		{
			return cur;
		}
		else
		{
			cur = cur->next;
		}
	}
	return NULL;
}
//在pos前面插入
void SLTInsert(SLNode** pphead, SLNode* pos, SLNDataType x)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	//assert((!pos && !(*pphead)) || (pos && (*pphead)));
	if (*pphead == pos)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLNode* newnode = CreateNode(x);
		prev->next = newnode;
		newnode->next = pos;
	}
}
//删除pos位置
void SLTErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(pos);
	assert(*pphead);
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
// 单链表在pos位置之后插入x
void SLTInsertAfter(SLNode* pos, SLNDataType x)
{
	SLNode* newnode = CreateNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}
// 单链表删除pos位置之后的值
void SLTEraseAfter(SLNode* pos)
{
	assert(pos->next != NULL);
	pos->next = pos->next->next;
	free(pos->next);
	pos->next = NULL;
}
//销毁
void SLTDestroy(SLNode** pphead)
{
	assert(pphead);
	SLNode* cur = *pphead;
	while (cur->next != NULL)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

4.带头双向循环链表的实现

4.1 认识带头双向循环链表

4.1.1 双向链表

我们之前认学习的单链表,是包含一个next指针指向下一个结点,而双向链表既有next指针,又有一个前指针指向前一个结点

4.1.2 循环链表

循环链表就是最后一个结点的next不指向NULL,指向第一个结点

4.1.3 带头链表

带头链表就是带哨兵位的头结点head,头结点不存数据

4.1.4 带头双向循环链表

  1. 无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
  2. 带头双向循环链表结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单

4.1.5 双向链表的优势和不足

双向链表的优势

  • 任意位置插入删除都是O(1)
  • 按需申请释放,合理利用空间,不存在浪费

问题

  • 下标的随机访问不方便O(N)

4.1.6 顺序表的优势和不足

顺序表的优势

  • 支持下标的随机访问O(1)

问题

  • 头插或中间插入的效率低O(N)
  • 空间不够需要扩容,有一定的消耗,且可能存在一定的空间浪费
  • 只适合尾插尾删

4.2 实现带头双向循环链表

同样我们创建三个文件来实现:

4.2.1 创建带头双向循环链表

我们在结构体中定义val存数据,prev指向前一个结点,next指向下一个结点

4.2.2 初始化

让phead->next和phead->prev都指向phead,给phead->val赋值为-1,最后返回phead

4.2.3 创建返回链表的头结点

4.2.4 打印链表

4.2.5 尾插

4.2.6 尾删

4.2.7 头插

4.2.8 头删

头结点不能删!!!

所以我们要assert(phead->next != phead)

4.2.9 查找

4.2.10 在pos位置前插入

特殊情况:

  • LTInsert(phead->next,x)就是头插
  • LTInsert(phead,x)就是尾插

4.2.11 删除pos位置

特殊情况:

  • LTErase(phead->next)就是头删
  • LTErase(phead->prev)就是尾删

4.2.12 销毁

4.3 实现代码

4.3.1 ListNode.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType val;
	struct ListNode* prev;
	struct ListNode* next;
}LTNode;

//初始化
LTNode* LTInit();
//创建返回链表的头结点.
LTNode* CreateLTNode(LTDataType x);
//打印
void LTPrint(LTNode* phead);

//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾删
void LTPopBack(LTNode* phead);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//头删
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//在pos位置前插入
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置
void LTErase(LTNode* pos);
//销毁
void LTDestroy(LTNode* phead);

4.3.2 ListNode.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"ListNode.h"
//初始化
LTNode* LTInit()
{
	LTNode* phead = CreateLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}
//创建返回链表的头结点.
LTNode* CreateLTNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newnode->val = x;
	newnode->next = NULL;
	newnode->prev = NULL;
	return newnode;
}
//打印
void LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	printf("哨兵位");
	while (cur != phead)
	{
		printf("%d<=>", cur->val);
		cur = cur->next;
	}
	printf("哨兵位\n");
}
//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = CreateLTNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	phead->prev = newnode;
	newnode->next = phead;
}
//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);
	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);
	tailPrev->next = phead;
	phead->prev = tailPrev;
}
//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = CreateLTNode(x);
	LTNode* first = phead->next;
	newnode->next = first;
	first->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;
	phead->next = second;
	second->prev = phead;
	free(first);
	first = NULL;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->val == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}
//在pos位置前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = CreateLTNode(x);
	LTNode* posprev = pos->prev;
	posprev->next = newnode;
	newnode->prev = posprev;
	newnode->next = pos;
	pos->prev = newnode;
}
//删除pos位置
void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posprev = pos->prev;
	LTNode* posnext = pos->next;
	posprev->next = posnext;
	posnext->prev = posprev;
	free(pos);
	pos = NULL;
}
//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		LTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
	phead = NULL;
}

5.数组下标为什么从0开始

 我们在学习数组时会有这个疑问:

数组元素的下标为什么从0开始而不从1开始呢?

从1开始更符合我们的日常习惯,比如生活中我们通常说第1个,而不是第0个

5.1 原因

对于数组元素的访问在操作系统层其实就是对特定内存偏移量的数据的访问

换而言之即如果想要访问一个数组的某一个元素的值那么首先就要计算它的地址偏移量

其大概的公式为:

a[k]_adress = base_address + k*type_size ;

倘若数组下标是从1开始那么地址计算公式即会转变为:

a[k]_adress = base_address + (k-1)*type_size ;

 这对于CPU来说多了一次减法操作

简单一句话: 就是为了方便 计算出每个元素的具体内存地址

因为数组变量 实际上在内存上储存的是这个数组变量中第一个元素的的首地址,而系统在取数组中某个元素的值时,必须要得到具体的那个元素的地址才能获取到对应的值

具体每个元素的内存地址 = 数组变量首地址 + 下标 x 每个元素占用的字节数

比如:

 int a[5]={10,11,12,13,14}

因为 int每个元素占用4个字节,所以 数组中每个相邻的元素内存地址都相差4,

那么每个元素的地址就等于前一个元素的地址 + 4

  • a[0] 的内存地址    =    a的地址 + 0 * 4  (第一个元素的地址计算结果  跟数组的首地址一样)
  • a[1] 的内存地址    =    a的地址 + 1 * 4     (下标是1,内存地址就就是首地址 偏移 4字节)
  • a[2] 的内存地址    =    a的地址 + 2 * 4    (下标是2,内存地址就首地址 偏移 8字节)
  • ..........
  • a[5]的内存地址    =    a的地址 + 5 * 4   (下标是5  内存地址就是首地址 偏移 20字节)

如果从1开始 就要减去1,多一步运算,效率自然不让直接点高

所以数组的索引(下标)从0开始是为了方便计算每个元素的地址

如果从1开始的话,运算起来没有从0开始方便

所以,大部分编程语言数组索引都是从0开始

5.2 拓展

为什么计算机语言中的下标都是从0开始的? - 知乎 (zhihu.com)

6.环型链表

6.1 环型链表是什么

 尾结点不指向NULL,指向头就是循环链表

那么带环链表就意味着尾结点的next可以指向链表的任意一个结点,甚至可以指向他自己

这里我们的算法思路唯一靠谱的还是快慢指针

slow一次走一步,fast一次走两步,当slow走到中间的时候,fast一定入环了,如果fast指向NULL,则该链表无环

当slow再走一半也就入环了,这个时候,由于slow走的慢,fast走的快,所以fast和slow最终会相遇的

6.2 快慢指针判断环形链表

我们在前面文章中写过用快慢指针判断链表是否带环:

leetcode:环形链表-CSDN博客

我们用的是slow指针一次走一步,fast指针一次走两步,当slow入环后开始了追击,每走一次距离缩短1,最终就会相遇

6.2.1 思考

但是我们思考一个问题:如果slow一次走一步,fast一次走三步,会不会相遇呢?

思考这个问题我们可以做一个假设:

假设环的长度是C,假设slow进环时,fast与slow之间的距离为N

6.2.2 推导思路

接着我们可以推一下:

如果slow一次走一步,fast一次走三步,每次追击距离缩小2

所以我们可以得出初步的结论:

  1. 如果N是偶数,就直接追上了
  2. 如果N是奇数,C是奇数,第一轮错过了,第二轮就追上了
  3. 如果N是奇数,C是偶数,就永远追不上

结论的第三条其实条件是不成立的,我们画图推一下:

所以这里我们就能得到一个结论

如果N是奇数,C是偶数,这个等式的条件是不成立的,所以不可能出第三种情况

6.2.3 结论

所以我们可以得出最终的结论:

  1. 如果N是偶数,就直接追上了
  2. 如果N是奇数,C是奇数,第一轮错过了,第二轮就追上了
  3. 不可能出现N是奇数,C是偶数的情况

所以如果slow一次走一步,fast一次走三步,一定能追上

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1533114.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

上海王梓标准件制造有限公司隆重参加上海紧固件专业展

随着全球工业制造业的高速发展&#xff0c;标准件行业做为基础部件的供应链环节越来越受到重视。标准件&#xff0c;这类微不足道的小零件&#xff0c;在维护工业世界的稳定和发展中是至关重要的。在这样一个紧要关头&#xff0c;上海王梓标准件制造有限公司&#xff08;下称“…

Redis 安装(二)

Redis安装说明 大多数企业都是基于Linux服务器部署项目&#xff0c;而且Redis官网也没有提供Windows版本的安装包&#xff0c;因此课程中我们会基于Linux系统来安装Redis。 此处选择Linux版本为Centos7。 Redis的官方网站地址&#xff1a;https://redis.io/ Redis的安装 切换…

51单片机产生波特率的计算分析,为什么常见11.0592mhz

数据手册中的资料 参考短文&#xff0c;机械周期 对波特率产生的计算分析 经常看到的公式是 b a u d f s o c / 12 / ( 计时器溢出需的数量 ) 2 s m o d ∗ 16 baud \dfrac{f_{soc}/12/(计时器溢出需的数量)}{2^{smod}*16} baud2smod∗16fsoc​/12/(计时器溢出需的数量…

【漏洞复现】1. WebLogic 反序列化漏洞(CVE-2019-2890)复现与分析

文章目录 1. 基础知识2. 复现2.1 漏洞介绍漏洞影响版本&#xff1a; 2.2 漏洞原理分析2.3 漏洞复现2.3.1 环境搭建2.3.2 漏洞验证2.3.3 漏洞利用2.3.4 POC分析 2.4 漏洞修复 1. 基础知识 WebLogic是美国Oracle公司出品的一个application server&#xff0c;确切的说是一个基于J…

麒麟 V10 一键安装 Oracle 19C 19.22 单机版

Oracle 一键安装脚本&#xff0c;演示 麒麟 V10 一键安装 Oracle 19C 19.22 单机版过程&#xff08;全程无需人工干预&#xff09;&#xff1a;&#xff08;脚本包括 ORALCE PSU/OJVM 等补丁自动安装&#xff09; ⭐️ 脚本下载地址&#xff1a;Shell脚本安装Oracle数据库 脚…

THM学习笔记—Bounty Hacker

nmap扫描&#xff0c;扫了一大堆但只有三个端口是开放的 试试ftp是否可以匿名登录 可以匿名登录&#xff0c;把里面的文件下载下来 查看里面的内容&#xff0c;猜lin为用户名&#xff0c;locks.txt为密码列表&#xff0c;使用hydra进行ssh登录。 找到密码了&#xff0c;进行ssh…

LoRa模块在野外科研与环境保护中的角色:科技守护自然之宝

随着科技的不断发展&#xff0c;LoRa&#xff08;低功耗广域网&#xff09;模块在野外科研与环境保护中正发挥着越来越重要的作用。其卓越的通信能力、低功耗特性以及良好的穿透能力&#xff0c;为科学家和环保人士提供了一种先进的技术手段&#xff0c;有助于更深入、更全面地…

LLM—Transformer作用及信息流

一、Transformer的作用 Transformer架构的精髓在于其创新性地采用了编码器与解码器的堆叠设计&#xff0c;这一设计巧妙地融合了多头自注意力机制&#xff08;Multi-Head Attention&#xff09;和位置前馈网络&#xff08;Position-wise Feed Forward Network&#xff09;两大核…

数据指标体系搭建指南:让数据说话,让决策更明智

如今数据已经成为企业运营中不可或缺的重要资源。无论是产品研发、市场营销还是决策制定&#xff0c;数据都发挥着至关重要的作用。因此&#xff0c;搭建一个科学、合理的数据指标体系&#xff0c;对于企业的长远发展具有重要意义。一个完善的数据指标体系&#xff0c;可以帮助…

基于爬虫对山西省人口采集+机器学习的可视化平台

文章目录 数据来源一、研究背景与意义二、研究目标三、研究内容与方法四、预期成果五、代码讲解六、全文总结 数据来源 1.所有原数据均来自&#xff1a;国家统计局-政府的数据网站 2.涉及到的一些预测数据是根据现有数据进行预测而来。 本文从数据来源&#xff0c;研究意义&am…

ideaSSM 高校公寓交流员管理系统bootstrap开发mysql数据库web结构java编程计算机网页源码maven项目

一、源码特点 idea 开发 SSM 高校公寓交流管理系统是一套完善的信息管理系统&#xff0c;结合SSM框架和bootstrap完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&…

3.7 RK3399项目开发实录-板载OpenWRT系统的使用(wulianjishu666)

STM32F103单片机从零到项目开发程序实例 下载链接&#xff1a;https://pan.baidu.com/s/1dWNskNinrMk4bxaE-jgHhQ?pwdymn3 1. OpenWRT 手册 1.1. 支持设备列表 主控板卡型号RK3568ROC-RK3568-PC/Station-P2 1.2. 登录 IP 、登录密码和 WIFI 名称 固件默认登录 IP 为 192.1…

数据结构—稀疏多项式相加

利用链表实现两个稀疏多项式相加。 代码 #include <iostream> using namespace std;// 定义多项式项结构体 typedef struct {int x; // 系数int y; // 指数 } Elemtype;// 定义链表节点结构体 typedef struct Node {Elemtype data;struct Node* next; } *LinkList, N…

如何使用 ArcGIS Pro 制作好看的高程渲染图

虽然 ArcGIS Pro 已经提供了很多好看的配色方案&#xff0c;但是如果直接对高程DEM进行渲染效果不是很理想&#xff0c;我们可以结合山体阴影让高程渲染图看起来更加立体&#xff0c;这里为大家介绍一下制作方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是…

C#,精巧实用的代码,调用GDI32.DLL绘制图形的源程序

C#画图既可以使用 System.Drawing 命名空间的各种基础类。在某些情况下,也可以直接调用 Windows 的公共基础链接库 GDI32.DLL。 1 GDI32.DLL图形设备接口 意图 Microsoft Windows图形设备界面(GDI)使应用程序能够在视频显示器和打印机上使用图形和格式化文本。基于Window…

停车管理系统asp.net+sqlserver

停车管理系统asp.netsqlserver 说明文档 运行前附加数据库.mdf&#xff08;或sql生成数据库&#xff09; 主要技术&#xff1a; 基于asp.net架构和sql server数据库&#xff0c; 功能模块&#xff1a; 停车管理系统asp.net sqlserver 用户功能有菜单列表 我的停车记录 专…

Qt学习笔记(一)——Qt初识

本文仅是 学习时记录的笔记&#xff0c;供自己复习时使用。 1.创建好文件(QWidget窗口) main.cpp中&#xff1a; 注&#xff1a;Widget类继承父类QWidget. widget.h中&#xff1a; Widget.cpp中&#xff1a; 打开Forms文件夹中的widget.ui文件&#xff1a; 打开左边编辑&am…

由浅到深认识Java语言(7):方法(函数)

该文章Github地址&#xff1a;https://github.com/AntonyCheng/java-notes 在此介绍一下作者开源的SpringBoot项目初始化模板&#xff08;Github仓库地址&#xff1a;https://github.com/AntonyCheng/spring-boot-init-template & CSDN文章地址&#xff1a;https://blog.c…

外包干了5天,技术退步明显。。。。

说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入广州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&a…

springcloud-Eureka注册中心

如果你要理解这个技术博客博客专栏 请先学习以下基本的知识&#xff1a; 什么是微服务什么是服务拆分什么是springcloud Springcloud为微服务开发提供了一个比较泛用和全面的解决框架&#xff0c;springcloud继承了spring一直以来的风格——不重复造轮子&#xff0c;里面很多的…