前言
某大厂内,小莱写完代码后翘着二郎腿靠在椅子上,正嘴里哼着小曲时。突然听到不远处传来了一声,"单链表都不会,这是基本功.......接着面吧!"。听到这句话,小莱顿时打了一激灵,端直坐了起来。
回想小莱的面试经历中也曾遇到过单链表的问题,曾经还在面试某大厂时被单链表反转pass过。
单链表作为最基础的数据结构,往往被忽略。大家的潜意识里可能认为越简单的东西大家都会,也就不会被作为考察与筛选条件。但事实却恰恰相反,单链表在面试环节中被问到的频率还是挺高的。且在原来的基础上又出现了很多拓展。
比如:
-
判断一个单链表是否有环
-
计算单链表环的长度
-
单链表整体反转
-
单链表间隔反转
......
但是无论单链表如何变化,追根溯源还是离不开最基础的实现方式。因此,这期小莱决定重新介绍下这个老面孔。可能有人会觉得对单链表很熟悉了,不用再看了。但是答应我接着往下看,一定有意想不到的收获!
本期主要分为以下几个部分:
-
链表结构
-
创建链表
-
插入节点(头插法、尾插法、中间插入)
-
删除节点(删除头、尾、中间节点)
-
链表全反转
画外音:本文具体代码实现通过公众号「IT界农民工」内回复「单链表」获取,有关单链表的拓展我们下期展开(毕竟篇幅有限,望大家谅解)。
对于链表的定义,小莱还想在这里啰嗦一句:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
接下来我们干点正事儿吧!!!
链表结构
首先我们来看下单链表的构造。看到这张图,可能会有人疑惑,这怎么和我从别的地方看到的表结构不一样呢,这是单链表吗?没错,这是小莱受redis底层双向链表所启发实现的(就是这么骚气)。
在这个表结构里,包含了哨兵节点和链表主体两部分。其中链表主体中的普通节点包含数据域和指针域。
哨兵节点由三个元素构成,分别为指向头节点和尾节点的head、tail指针域(可以理解为分别存储头节点和尾节点的地址),还有一个length字段,用来记录链表的长度。
画外音:是不是很多人在计算链表长度的时候偷偷使用了遍历链表的方式?(小样,被我识破了吧!)
在了解了链表结构之后,那么我们就动手创建一个单链表吧!
创建链表
经过链表结构的介绍,我们知道了两个节点。一个哨兵节点,还有一个普通节点。我们可以通过以下两个结构体分别来表示。
普通节点:
typedef struct lnode {
void *value;
struct lnode *next;
}lnode;
其中value用来存储数据,next用来指向下一个节点(可理解为存储下一个节点的地址)。
哨兵节点:
typedef struct list {
lnode *head;
lnode *tail;
int length;
}list;
head指向链表主体的第一个节点,tail指向最后一个节点,初始化时两个都可以设置为NULL,length表示链表长度。
创建了这两个节点后,我们就可以把哨兵节点和普通节点结合起来了。
当链表中只有一个节点的时候,那么哨兵节点的head 、tail分别用来存储普通节点的地址,此时length为1。
list->head = list->tail = node;
list->length = 1;
画外音:可能有同学对C语言不太了解,小莱在这里建议大家学学C语言基础知识。毕竟很多开源软件底层都会涉及到,如果想长远发展这是一条必经之路。
插入节点
到现在我们的链表已经初具雏形了,那么我们如何插入其它节点呢?
这里有三种情况,分别为:头插法、尾插法、中间插入。
我们把待插入的节点定义为 m 节点。
1、头插法
头部插入时,只需将m指针域指向头节点,然后将head指向新的m节点(注意这两个顺序不能颠倒)。
m->next = list->head;
list->head = m;
list->length++;
2、尾插法
尾部插入时,将尾节点指针域指向m,然后将tail指向新的m节点。
m->next = null;
list->tail->next = m;
list->tail = m;
list->length++;
3、中间插入法
中间插入时,将m节点指针域指向p的下一个节点(即q节点),将p节点指针域指向m节点(同样两个顺序不能颠倒)。
m->next = p->next;
p->next = m;
list->length++;
删除节点
1、删除头节点
这里分为两种情况:
a. 如果链表主体只有一个元素的话,直接将head、tail置为null即可。
list->head = list->tail = null;
list->length = 0;
b. 如果是一个以上的元素的话,需要将head指向头节点的下一个节点。
list->head = list->head->next;
list->length--;
2、删除尾节点
删除尾节点时,只需将尾节点前的节点的指针域置为空即可。
p->next = null;
list->tail = p;
list->length--;
3、删除中间节点
中间节点删除时,将p节点的指针域指向被删除节点所指向的下一个节点即可。对应图中为将p节点指针域指向m节点所指向的下一个节点。
p->next = m->next;
list->length--;
链表全反转
到这里相信大家对单链表都有了一定的了解,那么我们来看一道进阶题:
题目:
将链表:10->20->30->40
反转为:40->30->20->10
要求 :
时间复杂度为O(n),空间复杂度为O(1)
解答:
..............
如图,我们定义三个指针变量 p、q、m。当把q指向p的时候,链表会被截断,所以需要一个临时变量m来保存当前q节点的下一个节点。当进行一次调换后,指针整体向后移动一个节点。
代码实现:
p = list->head;
q = list->head->next;
while(m != NULL) {
m = q->next;
q->next = p;
p = q;
q = m;
}
list->tail = list->head;
list->tail->next = NULL;
list->head = p;
画外音:小莱再次提醒下大家,具体代码实现通过公众号「IT界农民工」回复「单链表」可以获取喔!!!
少侠留步
看你骨骼清奇,天赋异禀!
送你一份大厂面试秘籍!
点击「面试」即可获取!
关于作者
作者:大家好,我是莱乌,BAT搬砖工一枚。从小公司进入大厂,一路走来收获良多,想将这些经验分享给有需要的人,因此创建了公众号「IT界农民工」。定时更新,希望能帮助到你。