链表(单链表)
一、链表
1.为什么会有链表
2.链表的概念
3.链表的种类
二、单链表
单链表的实现
1、单链表的初始化
一、链表
1、为什么会有链表
在上次的文章中,我们讲到了关于顺序表的好处,比如它可以连续存储,它的尾插是很方便的,等等。但是,事物肯定不是十全十美的,顺序表也有它相应的问题:它的头部、中部的插入需要大范围的挪动数据,时间复杂度很高;增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。
因此先贤们为了解决这些问题,设计出了链表这一数据结构。
2.链表的概念与结构
3.链表的种类
二、单链表
单链表的实现
1、单链表的遍历
这里我们声明一个指针cur,从头结点指向的第一个结点开始,如果cur不为空就打印该节点里的值,并让cur走到下一个节点,直到走到NULL,循环结束,打印NULL。
void SLTPrint(SLNode* pehead)
{
SLNode* cur = pehead;//让cur指向头节点,代替头节点遍历
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
2.扩容
与顺序表一样,在插入时链表也需要扩容新节点。这里我们malloc一块新空间,并让新空间的值=x,让新空间指向NULL。
SLNode* CreateNode(SLNDataType x)
{
SLNode* newNode = (SLNode*)malloc(sizeof(SLNode));
if (newNode == NULL)
{
perror("malloc fail");
exit(-1);
}
newNode->val = x;
newNode->next = NULL;
}
3、链表的尾插
在解决了扩容问题,我们就可以着手实现插入。在实现尾插问题时,我们得先思考一个问题:这个链表是否为空?如果这个链表为空,那么,我们尾插的节点就是头节点,这时我们就可以让**ppehead(头结点的指针)指向newnode;如果这个链表不为空,我们就要找到这个链表的尾节点,在尾节点后插入。因此,我们就需要声明一个指针tail代替**pphead(头节点)实现对链表的遍历。原本尾节点的后面就是NULL,在找到尾节点后就让我们开辟的新节点newNode代替NULL。在新节点后本来也应指向NULL,但是因为在扩容时,我们就已经让newNode->next=NULL;所以无需再指向NULL。
void SLTPushBack(SLNode** ppehead, SLNDataType x)//尾插
{
SLNode* newNode = CreateNode(x);
//尾插要先找尾节点
{
*ppehead = newNode;
}
else
{
//假设链表不为空
SLNode* tail = *ppehead;
while (tail->next != NULL)
{
tail = tail->next;
}
//接着扩容一个新节点,让新节点指向空
tail->next = newNode;
}
}
4.为什么要使用二级指针(可忽略)
在最开始我们声明头节点(pehead),但是在尾插时我们使用的是*ppehead,即头节点pehead的指针。
相信大家都应该写过交换函数,我们在交换时,我们是调用的主函数外独立的一个函数,那么,在原函数里定义的临时变量出了该函数还有作用吗?这时我们是将主函数中我们要传递的数值传递过去吗?我们知道形参是实参的临时拷贝,所以我们要传递的是指针,即要交换的数的地址。
所以在对SLTPushBack函数调用时,我们也要传递plist的地址(&plist),但是pehead已经是头节点的指针(一级指针),所以我们只能用二级指针来确保我们成功调用该函数,得到想要的结果;
5.头插
在顺序表中,我们知道要实现头插需要大量的挪动数据,极大的影响效率。相较之下链表的头插就显得简单的多。
只要插入就一定需要开辟新节点,在开辟了新节点newNode后,这里我们只需要让新节点插到头节点之前,然后让新节点成为头节点即可。
void SLTPushFront(SLNode** ppehead, SLNDataType x)
{
SLNode* newNode = CreateNode(x);
newNode->next = *ppehead;
*ppehead = newNode;
}
6.尾删
尾删我们也要考虑一个问题,是不是直接找到尾节点删除掉就可以了?如果删除了尾节点,但是尾节点的上一个节点还指向原尾节点,这样指针不就成野指针了?因此我们必须先让尾节点的前一个节点指向NULL才能放心的尾删。
尾删这里我们有两种途径:1、双指针法,我们可以声明两个指针prev和tail。tail用来遍历数组找到尾节点,tail到尾节点时,prev是尾节点的前一个,这时我们就可以free掉尾节点,然后让prev保存的节点成为尾节点,让其指向空。
void SLTPopBack(SLNode** ppehead)
{
assert(*ppehead);
SLNode* prev = NULL;
SLNode* tail = *ppehead;
if ((*ppehead)->next == NULL)
{
free(*ppehead);
*ppehead = NULL;
}
else//多个节点,只有一个节点时,prev就是空了;
{
//找尾
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
2.单指针法:这里我们在循环时让tail->next->next != NULL作为循环继续的条件。当tail->next->next走到NULL时,tail->next刚好是尾节点,tail是尾节点的上一个节点,这时只要将 tail->next给free掉,这时tail成为尾节点,让tail->next=NULL即可。
void SLTPopBack(SLNode** ppehead)
{
assert(*ppehead);
SLNode* tail = *ppehead;
if ((*ppehead)->next == NULL)
{
free(*ppehead);
*ppehead = NULL;
}
else//多个结点
{
//找尾
while (tail->next->next != NULL)//第二种写法
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
但是,无论用哪种方式,我们依旧要考虑到,上述方法只适合多个节点。如果链表只有一个节点,我们就只能free头节点(free后依旧要让*ppehead指向NULL,防止野指针)。
7.头删
链表对于头部的处理都很有意思,头删我们可以声明一个指针tmp,让tmp 保存头节点,然后*ppehead走到下一个节点,再free掉tmp,原头节点就被删掉了,*ppehead成为了新头节点。
void SLTPopFront(SLNode** ppehead)
{
assert(*ppehead);
SLNode* tmp = *ppehead;
*ppehead = (*ppehead)->next;
free(tmp);
}
8、查找
遍历这个链表,如果某个节点的值=x,就返回这个节点,否则就继续找。
SLNode* SLTFind(SLNode* pehead, SLNDataType x)
{
SLNode* cur = pehead;
while (cur)
{
if (cur->val == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
}
9、任意插入
任意插入函数与头插尾插没有大的区别。当pos=0时,就是头插。在其他点插入也就是遍历到pos节点,然后插入。
void SLTInsert(SLNode** ppehead, SLNode* pos, SLNDataType x)
{
assert(*ppehead);
assert(pos);
SLNode * newNode = CreateNode(x);
//分情况
//pos=0,头插
if (*ppehead==pos)
{
SLTPushFront(*ppehead, x);
}
else
{
SLNode* prev = *ppehead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newNode;
newNode->next = pos;
}
}
10.任意删除
void SLTErase(SLNode** ppehead, SLNode* pos)
{
//分情况
if (*ppehead == pos)
{
//头删
SLTPopFront(*ppehead);
}
else
{
SLNode* prev = *ppehead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
11.test函数
这里是对单链表建立后的测试。
void test1()
{
SLNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
/*SLTPushFront(&plist, 9);
SLTPushFront(&plist, 8);
SLTPushFront(&plist, 7);
SLTPushFront(&plist, 6);*/
/*SLTPrint(plist);*/
}
void test2()
{
SLNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
SLTPopBack(&plist);
SLTPrint(plist);
}
void test3()
{
SLNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLNode * pos = SLTFind(plist, 3);
SLTInsert(&plist, pos, 12);
SLTPrint(plist);
}
void test4()
{
SLNode* plist = NULL;
SLTPushBack(&plist, 1);
SLTPushBack(&plist, 2);
SLTPushBack(&plist, 3);
SLTPushBack(&plist, 4);
SLTPushBack(&plist, 5);
SLTPrint(plist);
SLTPopFront(&plist);
SLTPrint(plist);
SLNode* pos = SLTFind(plist, 3);
SLTErase(&plist, pos);
SLTPrint(plist);
}
int main()
{
test1();
//test2();
//test3();
//test4();
return 0;
}
小结:本文简单介绍了链表,并对单链表的进行了简单的实现。如有不妥欢迎指出。