数据结构与算法入门_单链表

目录

链表简介

单链表的实现

1.动态申请节点

2.尾插的实现

3.尾删的实现

4.代码总结

每日一题

1.逻辑讲解

2.代码总结


链表简介

1.单链表简介

上篇文章我们讲解了顺序表,可随机存储,存储密度较高,但是对空间占用较大,改变容量不方便。因此我们将学习链表。

什么是链表

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

 

节点从堆中申请出来,地址可能连续也可能不连续 对于单链表的每一个结点,都需要有一个数据域用于存放节点的数据元素,需要一个指针域用于指向下一个结点。

单链表的实现

typedef int SLTDateType;
typedef struct SListNode
{
 SLTDateType data;
 struct SListNode* next;
}SListNode;

想象链表是一列火车,每个节点是一节车厢。BuySLTNode`就像一家“造车工厂”: 
步骤1:造车厢**:`malloc` 申请内存,造出一节新车厢。  
步骤2:检查质量**:如果内存不足(`malloc` 失败),工厂会报警(`perror`),返回空表示造车失败。  
步骤3:装货:将数据 `x` 装进车厢的 `data` 货仓。  
步骤4:独立车厢:新车厢的 `next` 指针设为 `NULL`,表示它暂时不连接其他车厢。

1.动态申请节点

LTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

为什么手动分配内存?  
  链表的节点是动态创建的,不像数组需要预先分配连续空间,**按需申请内存**更灵活。  
错误处理的意义:  
  如果 `malloc` 失败(比如内存耗尽),`perror` 会提示错误原因,避免后续操作崩溃。  
  实际开发中,调用者需检查返回的 `newnode` 是否为 `NULL`,否则可能导致空指针解引用。

2.尾插的实现

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);

	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		// 找尾
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}

		tail->next = newnode;
	}
}

想象成两个场景空车头和有车厢

情景1:空火车:如果整列火车是空的(`*pphead == NULL`),新车厢直接成为火车头。  
情景2:已有车厢:如果火车已经有车厢,司机(`tail` 指针)从火车头出发,一路走到最后一节车厢(`while` 循环),然后把新车厢挂在后面。tail->next = newnode;这里也有讲究

3.尾删的实现

void SLTPopBack(SLTNode** pphead) {
    assert(*pphead);  // 确保火车至少有一节车厢

    if ((*pphead)->next == NULL) {  // 只有一节车厢
        free(*pphead);            // 直接拆掉火车头
        *pphead = NULL;           // 火车头置空
    } else {                      // 多节车厢
        SLTNode* tail = *pphead;
        while (tail->next->next != NULL) {  // 找到倒数第二节车厢
            tail = tail->next;
        }
        free(tail->next);         // 拆掉最后一节
        tail->next = NULL;         // 倒数第二节的挂钩断开
    }
}

情景1:只剩一节车厢:直接拆掉火车头(`free(*pphead)`),整列火车消失(`*pphead = NULL`)。  
情景2:多节车厢:司机(`tail` 指针)从火车头出发,找到倒数第二节车厢(`while` 循环),然后拆掉最后一节车厢,并断开倒数第二节的挂钩(`tail->next = NULL`)。

深度思考:
为什么 `assert(*pphead)`?  
  防止对空链表执行删除操作。如果链表已经为空,`assert` 会终止程序,避免操作非法内存。  
实际开发中,可以改为返回错误码,但 `assert` 适合调试阶段暴露问题。  
循环条件的巧妙性:  
  `while (tail->next->next != NULL)` 确保 `tail` 最终停在倒数第二节车厢,从而能安全释放最后一节。如果写成 `while (tail->next != NULL)`,`tail` 会停在最后一节,此时无法修改倒数第二节的 `next` 指针。

4.代码总结

1. 链表的灵活性:  
   像火车车厢一样,链表可以动态增减节点,无需关心内存连续性,但需要付出遍历的代价。  
2. 指针的艺术:  
   通过二级指针 `pphead` 直接修改头指针,体现 C 语言“地址传递”的精髓。  
3. 防御性编程:  
   `assert` 和 `malloc` 检查是代码健壮性的基石,尤其在涉及内存操作时。

每日一题

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    int end1 = m - 1;     
    int end2 = n - 1;    
    int dst = m + n - 1;  

    // 从后往前比较,大的元素先放
    while (end1 >= 0 && end2 >= 0) {
        if (nums1[end1] > nums2[end2]) {
            nums1[dst--] = nums1[end1--];  // 把 nums1 的元素往后放
        } else {
            nums1[dst--] = nums2[end2--];   // 把 nums2 的元素往后放
        }
    }

    // 如果 nums2 还有剩余元素(nums1 已处理完)
    while (end2 >= 0) {
        nums1[dst--] = nums2[end2--];
    }
}

1.逻辑讲解

这道题的核心逻辑是从后往前填坑
1. 初始化指针:
 `end1` 指向 `nums1` 的最后一个有效元素(比如 `nums1` 有3个元素,`end1` 就是第2个位置)。
end2 指向 nums2 的最后一个有效元素。
dst指向合并后的最后一个位置(`nums1` 的总长度减1)。

2. 双指针比较:
从后往前遍历,比较 nums1[end1] 和 nums2[end2]。
谁大谁占坑:把较大的数放到 nums1[dst],然后对应的指针(end1 或 end2)和 dst`都往前移一步。
这个过程像“抢车位”,保证大的数先放到后面,合并后的数组依然是升序。

3. 处理剩余元素:
如果 nums2 还有元素没处理完(比如 nums1 的元素用完了),直接把剩下的 nums2 元素按顺序填到 nums1`的前面。

2.代码总结

这段代码像“倒着拼积木”,每次取最大的积木放在最后,直到所有积木拼完。既高效又优雅地解决了数组合并问题!

以上就是本篇博客全部内容啦,有任何不足欢迎大家在评论区交流指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值