数据结构之单链表

前文

昨天刚写完了顺序表的部分,今天加更给单链表也给上传一下,我觉得写代码理清各种事件的逻辑是很有成就感的,也希望我能继续把这个事情给做下去,努力做好

认识单链表

单链表也是线性表的表达方式之一,和数组不同的是,单链表里面的元素之间,内存不是连续的,是通过指针指向下一个元素。在单链表中,基本组成成分是节点,节点里面分成两个部分,一个是数据域,还有一个是指针域,指向下一个节点的位置。

#include <stdio.h>

typedef int Elemtype;
typedef struct SLlist{
    Elemtype Data;    //存放数据域
    struct SLlist* next;    //存放指针域
}SLlist;

正如代码中写的那样,将线性表封装在一个结构体里,里面的成员可以存放它的数据,还有一个结构体指针,这个结构体指针就是用来指向下一个节点的地址。

在线性表的使用中,有几个很重要,容易混淆的概念:头指针,头节点,尾指针,尾节点。

其中头节点是这个链表里面的第一个节点,里面存放了数据和下一个节点的地址,而头指针是一个结构体类型的指针,虽然形式上和节点有些与类似,但实际上里面存放的是头节点的地址,它不是这个链表的节点,只不过是在告诉人们,从这里指向了头节点。而尾节点则是链表的最后一个节点,通过设置尾节点的指针域,指向NULL或者头节点,可以构成一个普通的单链表或者循环单链表,尾指针则是指向尾节点的一个指针,和头指针作用基本一致,里面存放的是尾节点的地址

懂了这些,接下来就开始进一步学习链表的操作吧

链表的基本操作

链表的建立

链表和顺序表不同的一点是,在每一次加入新节点时,都要申请一份内存。

#include <stdio.h>
#include <stdlib.h>

typedef int Elemtype;
typedef struct SLlist{
    Elemtype Data;
    struct SLlist* next;
}SLlist;

//建立链表
SLlist* Create_List(Elemtype value){
    SLlist* New_List = (SLlist*)malloc(sizeof(SLlist));
    if(New_List == NULL){
        printf("获取内存失败\r\n");
        return NULL;
    }
    New_List->Data = value;
    New_List->next = NULL;
    return New_List;    //返回新节点的地址
}

在这里,我们通过malloc来申请一份内存,来存放新节点,在这里这个New_List既是一个新节点,也是这个新节点的地址,因为我们时通过它来存放了申请内存的首地址,而且这个指针还是结构体指针类型,里面有成员可以存放数据和下一个节点的地址NULL

所以我们可以明白,每个节点的名字都存放着自己这个节点的地址

在我们每一次插入新节点时,都要调用这个函数

链表的插入之尾插法

链表的插入有两种形式,一种是头插法,一种是尾插法。在这里我先讲尾插法,因为更符合生活现象。

尾插法中利用了两个指针,头指针和尾指针,将节点按照先后顺序依次插入在前一个节点的后面

//尾插法,新节点永远插在后面
SLlist* Tail_list(int n, Elemtype value[])	
//参数:n是指插入节点数目;value是节点的数据,以数组的形式传参,一次插入多个节点
{
	SLlist* head = NULL;	
//头指针,头指针指向头节点,也就是第一个节点
	SLlist* tail = NULL;	
//尾指针,尾指针指向尾节点,也就是最后一个节点,在尾插法中尾节点是必须要有的
	for (int i = 0; i < n; i++) {
		//创建新节点
		SLlist* newNode = Create_List(value[i]);
		if (head == NULL)	//如果插入的是第一个节点
		{
			head = newNode;
			tail = newNode;
		}
		else
		{
			tail->next = newNode;	
			//通过 tail 里保存的地址,找到前一个节点
			//然后把前一个节点的next字段写成newNode的地址

			tail = newNode;	//更新尾指针,存放的是地址
		}
	}
	return head;	//返回的是头指针,也就是头结点的地址
}

按照这样,如果插入的是第一个节点,原链表里面没有数据,那么得到的就是类似下图

其中的椭圆形为插入的第一个节点,左右两边分别为头指针和尾指针,该节点指向NULL

如果继续插入第二个节点,此时,由于链表不为空,则插入的新节点在原节点后面

我们可以看到,首先,先将原来的节点指向新节点,通过tail->next = newnode;再将尾指针更新,指向新节点,使得尾指针存放的是新节点的地址。

以此类推

然后整个函数返回头指针,也就是头节点的地址。

链表插入之头插法

与尾插法不同,头插法不需要用到尾指针,它是将新插入的节点放到表头

SLlist* Head_list(int n, Elemtype value[])
{
	SLlist* head = NULL; //头指针
	for (int i = 0; i < n; i++) {
		//创建新节点
		SLlist* newNode = createNode(value[i]);
		//核心
		newNode->next = head;
		head = newNode;	//更新头节点
	}
	return head;
}

当然,我个人不是特别爱用这个,更多的是采用尾插法的形式。

遍历单链表

void List_printf(SLlist* head)
{
    SLlist* current = head;
    while(current != NULL){
        printf("%d\r\n",current->Data);
        current = current->next;
    }
}

通过传入头节点,先判断链表是否为空,然打印节点的数据,再更新到下一个节点

链表中节点的删除

删除节点,有俩种方式,第一种,删除对应数据元素的第一个节点的;第二种,删除第i个节点。

在这里,我想先说明一下,我定义的链表的位置是从0开始的,也就是说,头节点位置是0,和数组类似。

删除节点,如果是在不是为头节点的情况下,那么,就是要将它的前节点和它的后节点连接起来,

再释放这个节点的内存;如果删除的是头节点,那就要改变头指针,将其指向下一个节点。这里就需要依靠二级指针的作用

第一种,删除对应数据元素的节点

void Delete_List(SLlist** head, Elemtype value)    //传入的参数是二级指针
{
    if(*head == NULL)//链表为空
    {
        printf("无法执行\r\n");
        return;
    }
    SLlist* current = *head;    //当前检查的节点
    SLlist* prev = NULL;       //上一个节点
    while(current != NULL && current->Data != value) 
   {
        prev = current;
        current = current->next;
   }
    if(current == NULL)    //没找到
    {
        printf("不存在该链表");
        return;
    }
    if(prev == NULL)    //删除的是头节点
    {
        //在这里才能体现二级指针的作用
        *head = current->next;
        free(current);
        printf("删除成功\r\n");
        return;
    }
    else    //删除的是中间节点或者尾节点
    {
        prev->next = current->next;
        free(current);
        printf("删除成功\r\n");
        return;
    }

}

删除操作要考虑删除的节点是不是头节点,如果是,那么就要改变头节点,所以需要传入一个二级指针,有类似操作的还有在某个位置插入某节点。

第二种,删除第i个节点

void Delete_Index_List(SLlist** head, int i)    //传入的参数是二级指针
{
    if(*head == NULL || i < 0)//链表为空
    {
        printf("无法执行\r\n");
        return;
    }
    int index = 0;
    SLlist* current = *head;    //当前检查的节点
    SLlist* prev = NULL;       //上一个节点
    while(current != NULL && index != i) 
   {
        prev = current;
        current = current->next;
        index++;
   }
    if(current == NULL)    //没找到
    {
        printf("不存在该链表");
        return;
    }
    if(index == 0)    //删除的是头节点
    {
        //在这里才能体现二级指针的作用
        *head = current->next;
        free(current);
        printf("删除成功\r\n");
        return;
    }
    else    //删除的是中间节点或者尾节点
    {
        prev->next = current->next;
        free(current);
        printf("删除成功\r\n");
        return;
    }

}

大体思路和第一种情况一样

总结一下删除节点的逻辑:

首先先判断一下这个节点是否为空(有时还要判断删除的位置是否正确),如果不是则继续,创造两个结构体指针,一个指向当前,一个指向现在之前(有时需要再写上一个数字,用于累计观察)
当前节点从头节点开始,而前节点从NULL开始
然后写上一个无限循环,来开始慢慢推进,直到当前节点为空或此时节点的数据域对应上,那就跳出循环
第一种可能,这个链表里面没有我要找的元素,所以推进到最后,当前节点变成了NULL
第二种可能,此时要删掉的节点是头节点,那么条件就是前一个节点为NULL(或者删除位置为0),然后语句体里面写上:更新头节点,释放当前节点。
第三种可能,此时要删掉的节点是中间节点或者尾节点,那么就要跳过当前节点,再释放当前节点

链表的插入

链表的插入要实现,也是和删除样,要考虑头节点,所以就需要二级指针作为入口

int Insert_List(SLlist** head, int pos, Elemtype value) {
	// 检查位置是否合法
	if (pos < 0) {
		printf("位置不能为负数\r\n");
		return 0;  // 返回0表示失败
	}

	// 创建新节点
	ListNode* newNode = createNode(value);

	// 情况1:插在头部(pos == 0)
	if (pos == 0) {
		newNode->next = *head;	//将新节点的指针域指向之前的头节点
		*head = newNode;		//更新头节点
		printf("已插入 %d 到位置 %d\n", value, pos);
		return 1;  // 成功
	}

	// 情况2:插在中间或尾部
	ListNode* current = *head;
	int index = 0;

	// 寻找第pos-1个节点(也就是插入位置的前一个)
	while (current != NULL && index < pos - 1) {
		current = current->next;
		index++;
	}

	// 如果没走到指定位置(比如链表太短)
	if (current == NULL) {
		printf("位置 %d 超出链表范围!\n", pos);
		free(newNode);  // 记得释放未使用的节点
		return 0;
	}
	// 此时current是第 pos-1 个节点
	newNode->next = current->next;    //新节点的指针域指向原来这个位置的节点
	current->next = newNode;          //第pos-1个节点的指针域更新,指向新节点

	printf("已插入 %d 到位置 %d\n", value, pos);
	return 1;  // 成功
}

链表的插入逻辑:

首先判断插入的位置是否合法,如果合法则继续下一步,用一个结构体指针来接受这个新插入的节点
然后开始判断如果它插在头部(假设位置是0),那么就要将这个新节点的指针域指向之前的头节点,再更新头指针
假设位置不是0:用一个结构体指针来代替头节点,再用一个整数来计数,目的找到插入位置的前一个节点,进行无限循环慢慢推进
直到不存在该节点或者找到对应节点就退出
第一种没找到,此时current == NULL
第二种:找到了,此时将新节点的指针域指向原节点的地址;再将插入位置前一个节点的指针域更新,指向新节点

链表的查找

链表的查找和删除一样,也是要分成两种情况,第一种是根据数据值,来查找对应的第一个元素,第二种则是根据位置,查找第i个元素

第一种

//查找节点(返回节点指针)
SLlist* search(SLlist* head, Elemtype value)//传入头节点
{
	SLlist* current = head;
	while (current != NULL) {
		if (current->Data == value) {
			return current;	//返回结点指针
		}
		current = current->next;
	}
	return NULL;
}
//查找节点(返回当前位置)
int searchIndex(SLlist* head, Elemtype value)
{
	SLlist* current = head;
	int index = 0;
	while (current != NULL)
	{
		if (current->Data == value)
		{
			return index;
		}
		current = current->next;
		index++;
	}
	return -1;	//没找到
}

第二种

//查找节点(返回该节点的地址)
SLlist* Index_List(SLlist* head, int i)
{
	ListNode* current = head;
	int index = 0;
	while (current != NULL)
	{
		if (index == i)
		{
			return current;
		}
		current = current->next;
		index++;
	}
	return NULL;	//没找到
}

链表排序

链表的排序比较难理解,我本人常用的方式有两种情况,第一种就是冒泡排序的改装;第二种则是

通过链表的特性直接改变指针域实现排序

设定我现在按照升序的方式来排序

第一种

void bubbleSortList(SLlist* head) {
	if (head == NULL) return;
	ListNode* end = NULL; // 每轮最大值冒泡到末尾,后续不用比较

	while (head->next != end) { // 外层:控制轮数
		ListNode* current = head;
		while (current->next != end) { // 内层:相邻比较
			if (current->Data > current->next->Data) {
				// 交换数据
				Elemtype temp = current->Data;
				current->Data = current->next->Data;
				current->next->Data = temp;
			}
			current = current->next;
		}
		end = current; // 缩小比较范围,则end及其后面的都是正常排好顺序的,end逐步向链表左边移动
	}
}

第二种

SLlist* insertionSortList(SLlist* head) {
	if (!head || !head->next) 
    return head;

	SLlist dummy;          // 虚拟头节点
	dummy.next = NULL;
	SLlist* current = head;

	while (current != NULL) {
		SLlist* next = current->next; // 保存下一个节点(防止断链)

		// 在 dummy 链表中找插入位置
		SLlist* prev = &dummy;
		while (prev->next != NULL && prev->next->Data < current->Data) {
			prev = prev->next;
		}

		// 插入 current 到 prev 之后
		current->next = prev->next;
		prev->next = current;

		current = next; // 继续处理下一个
	}

	return dummy.next;    //返回虚拟头节点的下一个节点,也就是我们需要的第一个节点
}

这里这个链表的排序我打算再重新开个下章节讲一下,就和下次的双链表一起写,我今天写这个感觉头有点充血了,人都写傻了,等下后面我看一下这里该怎么将好理解一点。

 这个是我之前写的逻辑,可以先这么理解一下

总结

单链表的介绍和基本功能就讲的差不多了。由于作者学校这几天考试繁多,可能更新较慢,不过我会把它更的,这个我觉得可以

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值