C语言深入讲解链表的使用

目录
  • 一、链表的概念
  • 二、链表的分类
    • 1. 单向或者双向链表
    • 2. 带头或者不带头(是否有自带哨兵位头结点)
    • 3. 循环或者非循环链表
    • 4. 无头单向非循环链表和带头双向循环链表
  • 3、链表的实现(代码和注释)
  • 4、链表oj题(小试牛刀)
  • 总结

现实生活中的火车就像一个完整的链表,现在我们来深入理解一下链表这个数据结构。

一、链表的概念

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

注:

1、从上图中可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。

2、从现实中的结点一般是通过malloc函数申请的,所以其内存分配是在堆区。

3、从堆上申请的空间,是按照一定的策略来分配的,则一个节点的大小为8个字节。

二、链表的分类

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

1. 单向或者双向链表

2. 带头或者不带头(是否有自带哨兵位头结点)

第二个链表的d1指向了我们的哨兵位头结点。

3. 循环或者非循环链表

4. 无头单向非循环链表和带头双向循环链表

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

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

3、链表的实现(代码和注释)

无头+单向+非循环链表增删查改实现

头文件:

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//要求存储的数据从0开始,依次存储
//静态的顺序表
//问题:开小了,不够用,开大了,存在浪费。
//struct SeqList
//{
//	int a[N];
//	int size;//记录存储了多少个数据。
//};
typedef int SLDateType;//宏定义我们的 SLDateType是int类型的
//动态的顺序表
typedef struct SeqList
{
	SLDateType* a;
	int size;	//存储数据个数
	int capacity;//存储空间大小
}SL, SeqList;
void SeqListPrint(SeqList* psl);//链表的打印
void SeqListInit(SeqList* psl);//链表的初始化
void SeqListDestroy(SeqList* psl);//链表的销毁
void SeqListCheckCapacity(SeqList* psl);//检查内存空间是否足够
//时间复杂度是o(1)
void SeqListPushBack(SeqList* psl, SLDateType x);//链表的尾插
void SeqListPopBack(SeqList* psl);//链表的尾删
//时间复杂度是o(n)
void SeqListPushFront(SeqList* psl, SLDateType x);//链表的头插
void SeqListPopFront(SeqList* psl);//链表的头删
void SeqListInsert(SeqList* psl, size_t pos, SLDateType x);
// 删除pos位置的数据
void SeqListErase(SeqList* psl, size_t pos);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDateType x);

链表的函数实现部分的代码:

#include"SeqList.h"
#include<assert.h>
void SeqListPrint(SeqList* psl)//结构体指针传参
{
	for (int i = 0; i < psl->size; ++i)
	{
		printf("%d ", psl->a[i]);
	}
	printf("\n");
}
void SeqListInit(SeqList* psl)
{
	assert(psl);//断言psl不为空
	psl->a = NULL;//a就相当于是链表的头
	psl->size = 0;
	psl->capacity = 0;
}
void SeqListDestroy(SeqList* psl)//链表的删除
{
	assert(psl);
	free(psl->a);//free掉链表中a这个节点的位置
	psl->a = NULL;//将a指向空
	psl->capacity = psl->size = 0;//将链表的内存大小置为0
}
void SeqListCheckCapacity(SeqList* psl)//检查链表的内存,如果不够就增容。
{
	assert(psl);
	if (psl->size == psl->capacity)
	{
		//capacity == 0,所以要先特判一下capacity 的值
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;//初始节点数为4,如果内存现在为0就扩大一倍
		SLDateType* tmp = realloc(psl->a, sizeof(SLDateType) * newCapacity);//申请空间
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			psl->a = tmp;
			psl->capacity = newCapacity;//原来的空间变为新空间
		}
	}
}
void SeqListPushBack(SeqList* psl, SLDateType x)
{
	//如果满了,要扩容
	if (psl->size == psl->capacity)
	{
		//capacity == 0,所以要先特判一下capacity 的值
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDateType* tmp = realloc(psl->a, sizeof(SLDateType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			psl->a = tmp;
			psl->capacity = newCapacity;
		}
	}
	psl->a[psl->size] = x;
	psl->size++;
}
void SeqListPopBack(SeqList* psl)
{
	assert(psl);
	if (psl->size > 0)
	{
		psl->size--;//尾删就size--就好了
	}
}
void SeqListPushFront(SeqList* psl, SLDateType x)
{
	assert(psl);
	SeqListCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)//所有的数据往后挪1位
	{
		psl->a[end + 1] = psl->a[end];
		--end;
	}
	psl->a[0] = x;//两种操作是等价的
	psl->size++;
	//SeqListInsert(psl, 0, x);在头部插入。
}
void SeqListPopFront(SeqList* psl)
{
	assert(psl);
	if (psl->size > 0)
	{
		int begin = 1;
		while (psl->size > begin)
		{
			psl->a[begin - 1] = psl->a[begin];
			++begin;
		}
		--psl->size;
	}
}
void SeqListInsert(SeqList* psl, size_t pos, SLDateType x)
{
	// 暴力检查
	assert(psl);
	// 温和检查
	if (pos > psl->size)
	{
		printf("pos 越界:%d\n", pos);
		return;
		//exit(-1);
	}
	// 暴力检查
	//assert(pos <= psl->size);
	SeqListCheckCapacity(psl);
	//int end = psl->size - 1;
	//while (end >= (int)pos)
	//{
	//	psl->a[end + 1] = psl->a[end];
	//	--end;
	//}
	size_t end = psl->size;
	while (end > pos)
	{
		psl->a[end] = psl->a[end - 1];
		--end;
	}
	psl->a[pos] = x;
	psl->size++;
}

4、链表oj题(小试牛刀)

1、leetcode203移除链表元素力扣

包含三种情况

画个图分析:

思路:情况一和情况二都可以用prev和cur指针遍历数组的两个元素, if cur指针不等于6,prev指针和cur指针都往前走,如果cur = 6,prev跳到cur的下一个位置

如果是第三种情况,则需先找到head != val的位置,再重复进行如上操作。

代码示例:

struct ListNode* removeElements(struct ListNode* head, int val)
{
    struct ListNode* prev = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        if(cur->val != val)//当cur这个位置的值不等于val时往下走
        {
            prev = cur;//prev跳到cur位置
             cur = cur->next;//cur指针继续往下走
        }
        else
        {
            struct ListNode* next = cur->next;//定义一个新的指针
            if(prev == NULL)//头删,head为空的状态
            {
                free(cur);
                head = next;//继续往后面走
                cur = next;
            }
            else
            {
                free(cur);//free掉cur这个节点
                prev->next = next;//跳过了cur这个点
                cur = next;//cur继续往后走
            }
        }
    }
    return head;
}

2、leetcode206反转链表力扣

思路1:反转指针方向

思路二:用三个指针, n1, n2, n3 分别存放NULL, head, head->next;

代码示例:

struct ListNode* reverseList(struct ListNode* head)
{
    if(head == NULL) return NULL;
    struct ListNode* n1, *n2, *n3;
    n1 = NULL;
    n2 = head;
    n3 = n2->next;//n3的地址是n2的下一位
    while(n2)
    {
        n2->next = n1;//n2 的下一位指向 n1;起到掉头的作用
        n1 = n2;
        n2 = n3;
        if(n3)
        n3 = n3->next;
    }
    return n1;
}

方法2:头插法

殊途同归。newhead 相当于之前的n1, cur = n2, next = n3;
struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* NewHead = NULL;
    struct ListNode* cur = head;
    while(cur)
    {
        struct ListNode* next = cur->next;
        //头插
        cur->next = NewHead;//代表链表的指向方向。
        NewHead = cur;//接着把地址传过来(更新)
        cur = next;//(更新)
    }
    return NewHead;
}

3、leetcode 876链表的中间结点力扣

思路:快慢指针法

struct ListNode* middleNode(struct ListNode* head)
{
    struct ListNode* slow, * fast;
    slow = fast = head;//刚开始slow和fast指针都指向头
    while(fast && fast->next) //想的是结束的条件,写的是继续的条件
    {
        slow = slow->next;
        fast = fast->next->next;//fast 每次走两步
    }
    return slow;
}

总结

这里我带大家从链表的概念,链表的分类,链表的简单实现以及3题oj题四个方面带大家认识链表

到此这篇关于C语言深入讲解链表的使用的文章就介绍到这了,更多相关C语言链表内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言实题讲解快速掌握单链表上

    目录 1.移除链表元素 2.反转链表 3.链表的中间节点 4.链表中倒数第k个节点 5.合并两个有序链表 6.链表分割 1.移除链表元素 链接直达: 移除链表元素 题目: 思路: 此题要综合考虑多种情况,常规情况就如同示例1,有多个节点,并且val不连续,但是非常规呢?当val连续呢?当头部就是val呢?所以要分类讨论 常规情况: 需要定义两个指针prev和cur,cur指向第一个数据,prev指向cur的前一个.依次遍历cur指向的数据是否为val,若是,则把prev的下一个节点指向cur的下

  • ​​​​​​​C语言实现单链表基本操作方法

    目录 存储结构 基本功能 头插法创建单链表 尾插法创建单链表 获取指定位置的元素 在指定位置插入元素 删除指定位置的元素 获取单链表的长度 合并两个非递减的单链表 晴链表 遍历打印单链表 附上完整代码 存储结构 typedef int dataType://爱护据类型 typedef struct Node { DataType data; // 结点数据 struct Node *next; // 指向下一个结点的指针 } Node, *LinkList; 基本功能 头插法创建单链表void

  • C语言超详细i讲解双向链表

    目录 一.双向链表的概念 二.双向链表的实现 三.链表与顺序表的差别 四.链表oj 总结 一.双向链表的概念 1.概念:概念:双向链表是每个结点除后继指针外还有⼀个前驱指针.双向链表也有带头结点结构和不带头结点结构两种,带头结点的双向链表更为常用:另外,双向链表也可以有循环和非循环两种结构,循环结构的双向链表更为常用. 二.双向链表的实现 头文件List.h #pragma once #include<stdio.h> #include<assert.h> #include<

  • C语言一篇精通链表的各种操作

    目录 前言 一.链表的介绍 1.什么是链表 2.链表的分类 2.1.根据方向 2.2.头结点 2.3.循环/非循环 二.链表的实现 1.结构体 2.开辟结点 3.打印 4.尾插 5.头插 6.测试 7.头删/尾删 8.查找 9.在pos的前面插入x 10.删除pos位置的值 三.主函数Test 结束语 前言 关于线性表的一些相关介绍,大家可以看看我们之前写的点我-链表 点我-顺序表,里面有一些相关的知识介绍,都是比较基础的,一些比较常见的操作里面也有具体的介绍与实现到,然后呢,今天我们学习的是链

  • C语言详解如何实现带头双向循环链表

    目录 创建链表存储结构 创建结点 链表的初始化 双向链表的打印 双向链表尾插 双向链表尾删 双向链表头插 双向链表头删 双向链表查找 双向链表pos前插入结点 双向链表删除pos位置的结点 双向链表的销毁 顺序表和链表的区别 2022042311415360.{C}{C}png" /> 创建链表存储结构 我们需要创建一个结构体来存储一个链表结点的相关信息. typedef int ListDataType;//将ListDataType先定义为int类型,根据需要可以改为不同的类型 //创

  • C语言循环链表的原理与使用操作

    目录 一.引入 二.循环链表的定义 三.单链表与循环链表对比 3.1图示对比 3.2代码对比 四.循环链表的操作 4.1循环链表的初始化 4.2循环链表的建立 4.2.1头插法建立循环链表 4.2.2尾插法建立循环链表 4.3循环链表的插入操作 4.4循环链表的删除操作 4.5循环链表的销毁 4.5.1无限头删 4.5.2两个指针变量 4.6其他操作 五.总结 六.全部代码 一.引入 在单链表中我们是把结点遍历一遍后就结束了,为了使表处理更加方便灵活,我们希望通过任意一个结点出发都可以找到链表中

  • C语言实题讲解快速掌握单链表下

    目录 1.移除链表元素 2.反转链表 3.链表的中间节点 4.链表中倒数第k个节点 5.合并两个有序链表 6.链表分割 1.移除链表元素 链接直达: 移除链表元素 题目: 思路: 此题要综合考虑多种情况,常规情况就如同示例1,有多个节点,并且val不连续,但是非常规呢?当val连续呢?当头部就是val呢?所以要分类讨论 常规情况: 需要定义两个指针prev和cur,cur指向第一个数据,prev指向cur的前一个.依次遍历cur指向的数据是否为val,若是,则把prev的下一个节点指向cur的下

  • C语言双向链表的原理与使用操作

    目录 一.引入 二.双向链表的定义 三.双向链表与单链表对比 3.1图示对比 3.2代码对比 四.双向链表的操作 4.1双向链表的创建 4.2双向链表的插入 4.3双向链表的删除 4.4双向链表的销毁 五.总结 六.全部代码 一.引入 我们在单链表中,有了next指针,这个指针是用来指向下一个节点的,如果我们需要查找下一个结点的时间复杂度为o(1),如果我们需要查找上一个节点的时候,那么时间复杂度就变为o(n)了,需要从头进行遍历一遍:这时我们就会想:如果可以向前查找就方便了许多,因此我们引入了

  • C语言算法学习之双向链表详解

    目录 一.练习题目 二.算法思路 1.设计浏览器历史记录 2.扁平化多级双向链表 3.展平多级双向链表 4.二叉搜索树与双向链表 一.练习题目 题目链接 难度 1472. 设计浏览器历史记录 ★★★☆☆ 430. 扁平化多级双向链表 ★★★☆☆ 剑指 Offer II 028. 展平多级双向链表 ★★★☆☆ 剑指 Offer 36. 二叉搜索树与双向链表 ★★★★☆ 二.算法思路 1.设计浏览器历史记录 1.这是一个模拟题: 2.初始化生成一个头结点,记录一个当前结点: 3.向前 和 向后 是两

  • C语言深入讲解链表的使用

    目录 一.链表的概念 二.链表的分类 1. 单向或者双向链表 2. 带头或者不带头(是否有自带哨兵位头结点) 3. 循环或者非循环链表 4. 无头单向非循环链表和带头双向循环链表 3.链表的实现(代码和注释) 4.链表oj题(小试牛刀) 总结 现实生活中的火车就像一个完整的链表,现在我们来深入理解一下链表这个数据结构. 一.链表的概念 概念:链表是一种物理存储结构上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链 接次序实现的 . 注: 1.从上图中可看出,链式结构在逻辑上是连续

  • C语言数据结构单链表接口函数全面讲解教程

    目录 前言 一.链表的概念及结构 1.概念 二.链表的使用 1.遍历整个链表 2.尾插 3.头插 4.头删 5.尾删 6.任意位置插入数据 7.任意位置删除数据 后记 前言 上一期数据结构专栏我们学习了顺序表后:C语言数据结构顺序表 在运用时,细心的同学可能会发现,如果要头插.尾插或者任意位置.如果原先的空间已经被占满了,你是需要扩容的,动态链表扩容往往是2倍,但是扩容后,如果后面没有使用完全扩容后空间就会造成空间浪费,为了解决这个问题,我们今天将学习链表. 提示:以下是本篇文章正文内容,下面案

  • C语言数据结构实现链表去重的实例

    C语言数据结构实现链表去重的实例 题目及分析 链表去重 时间限制 300 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standard 给定一个带整数键值的单链表L,本题要求你编写程序,删除那些键值的绝对值有重复的结点.即对任意键值K,只有键值或其绝对值等于K的第一个结点可以被保留.同时,所有被删除的结点必须被保存在另外一个链表中.例如:另L为21→-15→-15→-7→15,则你必须输出去重后的链表21→-15→-7.以及被删除的链表-15→15. 输入格式: 输入

  • C语言深入浅出讲解顺序表的实现

    目录 1.线性表 2.顺序表 2.1 概念及结构 2.2 提供接口 2.3 接口实现 今天起开始编写数据结构中的各种数据结构及算法的实现,说到顺序表,我们首先得了解下线性表. 1.线性表 线性表(linear list)是n个具有相同特性的数据元素的有限序列. 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表.链表.栈.队列.字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线.但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储. 2.顺序表

  • C语言简明讲解队列的实现方法

    目录 前言 队列的表示和实现 队列的概念及结构 代码实现 束语 前言 大家好啊,我又双叒叕来水博客了,道路是曲折的,前途是光明的,事物是呈螺旋式上升的,事物最终的发展结果还是我们多多少少能够决定的,好啦,吹水结束,这与这篇博客的主题并没有太多联系.关于栈和队列这一板块本来是想不写(就是想偷懒),但是想了想,觉得这样不太好,关于数据结构这一块可能会有缺失,所以最终还是决定写,必须补齐这一块,恰好最近有时间写博客,所以还是写了,这篇博客将介绍队列的知识点,理解链表那一块的操作后,栈和队列的相关操作还

  • C语言深入讲解栈与堆和静态存储区的使用

    目录 一.程序中的栈 二.函数的调用过程 三.函数调用的栈变化 四.函数调用栈上的数据 五.程序中的堆 六.程序中的静态存储区 七.小结 一.程序中的栈 栈是现代计算机程序里最为重要的概念之一 栈在程序中用于维护函数调用上下文 函数中的参数和局部变量存储在栈上 栈保存了一个函数调用所需的维护信息 参数 返回地址 局部变量 调用上下文 二.函数的调用过程 每次函数调用都对应着一个栈上的活动记录 调用函数的活动记录位于栈的中部 被调函数的活动记录位于栈的顶部 三.函数调用的栈变化 从main() 开

  • C语言实现动态链表的示例代码

    目录 结构体定义已经函数声明 函数实现 创建一个链表 判断链表是否为空 获得链表中节点的个数 在某个特定的位置插入一个元素 获得指定下标的节点的元素 删除一个节点 链表逆序 链表的清空 链表的销毁 链表的遍历 按特定的元素查找节点 按某些特定的条件删除所有符合情况的节点 链表的排序 总结 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点包括两个部分:一个是存储

  • Java语言实现反转链表代码示例

    问题描述 定义一个函数,输入一个链表的头结点,反转该链表并输出反转后的链表的头结点.链表结点如下: public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } } 思路1: 要想反转链表,对于结点i,我们要把它的next指向它的前趋,因此我们需要保存前趋结点,同时,如果我们已经把i的next重新赋值,会无法找到i的后继,因此,在重新赋值之前,我们要保存i的后继. 代码:

  • C语言数据结构旋转链表的实现

    C语言数据结构旋转链表的实现 实例: 给出链表1->2->3->4->5->null和k=2 返回4->5->1->2->3->null 分析: 感觉很直观,直接把分割点找出来就行,记得k可能大于len,要取模 代码: /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x

  • C语言数据结构 link 链表反转的实现

    C语言数据结构 link 链表反转的实现 链表反转,示例如下: 偶数个输入:a->b->c->d->e->f 偶数个输出:e->f->c->d->a->b or 奇数个输入:a->b->c->d->e->f->g 偶数个输出:g->e->f->c->d->a->b #include <stdio.h> #include <malloc.h> #incl

随机推荐