【数据结构】单链表——增删查改
链表
链表属于一种链式存储的线性表,用一组地址任意的存储单元存放线性表的数据元素,成存储单元的一个节点;
链表分为:有/无头结点的、有/无环的、单/双链表;由此可以排列组合出:无头结点无环的单链表、无头结点有环的单链表、有头结点无环的单链表、有头结点有环的单链表、无头结点无环的双链表、无头结点有环的双链表、有头结点无环的双链表、有头结点有环的双链表 8种链表
本篇主要讨论无头结点无环的单链表
在这里,head不只是节点,同时也是链表身份的标识。
创建头文件
在此基础上建立函数,
1、需要初始化链表——LinkListInit
需要注意的一点是:这里的要用ListNode** head而不是ListNode* head ,因为如果用后者的话,head是形参,只是实参的拷贝,对外不生效,所以在这里要用二级指针。
2、在链表末尾插入一个节点——LinkListPushBack
思路:
- 是空链表,则直接使头结点指向该节点就好。
- 不是空链表,先遍历至尾节点(下一个节点指向NULL的节点),然后在使该节点指向新的节点,新节点指向NULL。
在写尾插函数之前,需要先完成另一个函数
void LinkListCreateNode(LinkType value);
该函数的作用是,给一个新的值创建新的节点;
从图里可以看到,我们选用了malloc而没有用被注释掉的代码来开辟空间,原因是:
malloc出的空间是在堆上的,除了手动free之外,是会一直存在的,但是第二种方法中开辟出的node是在栈上的,出了该函数就会被自动释放,ptr就会变为野指针。
代码:
从上面代码可以发现,在最后并没有使新的节点指向NULL,这是因为在Create函数中,已经使ptr->next=NULL了,所以在尾插函数中,可以不做该操作。
测试,得到如下结果:
由结果可以看出,abcd的地址不连续,这也是链表的特点之一。
测试中,需要用到的一个函数
void LinkListPrint(LinkNode* head, const char *msg);
该函数的作用是打印函数的功能("%s")、链表中节点的值("%c")和节点的地址("%s"),最后指向NULL
有了这个函数,测试会方便很多,结果也清楚很多。
3、从链表尾部删除一个节点——void LinkListPopBack
思路:
- 空链表直接返回。
- 只有一个节点的链表,直接删除头结点。
- 有两个及两个以上节点的链表,①使倒数第二个指针下一个指向NULL,②将原本的最后一个指针释放,防止内存泄漏。
- 注意:为了能够释放最后一个节点,需要先把最后的节点保存起来,然后让倒数第二个指针指向NULL,这样才可以释放最后的节点。
画图表示更清晰:
这里也需要写一个函数
void LinkListDestory(LinkNode ptr)){
free(ptr);
}
该函数的功能是删除节点
为什么free(ptr)而不是head,看下图:
代码:
测试结果:
可以看到两个元素以上的链表尾删失败
4、在链表前端插入一个节点——LinkListPushFront
思路:
- 空链表,直接插入。
- 非空链表的话,①新建一个指针cur存储新插入的值,②cur->next指向原本的头节点,③头指针指向新的指针cur。
画图解释会更清晰,假如刚开始是这样的链表,新插入的值为value,新建的指针为cur:
则变为:
代码:
测试结果:
5、在链表前端删除一个节点——LinkListPopFront
思路:
- 空链表,直接返回。
- 非空链表,①将头结点存储在新建的指针中(delete),②头指针指向原来头结点的下一个节点,③删除原本的头结点。
画图表示:
代码:
测试结果:
6、在链表前端查找某元素的坐标——LinkListFind
思路:
- 空链表直接返回。
- 新建一个指针cur指向头结点,比较cur对应的元素和要寻找的元素value,相同的话就返回cur,不同则cur指向cur->next,直到cur指向NULL。
画图如下:
代码:
细心一点会发现,这里和前面有两点不同:
- 用了LinkNode* 而不是void,这是因为找到之后返回值为指针cur。
- 定义时用了LinkNode* head而不是LinkNode** head,这是因为本函数只需要找到并返回坐标,不会对头指针做出修改,所以只需要传形参即可。
测试结果: