目录
头指针和头结点
线性链表的存储结构,整个链表的存取必须从头结点开始。
头指针指向头结点,头结点的数据域不存放任何数据,也可以存储线性表长度等信息,头结点的指针域存储指向第一个节点的指针。
头结点的作用:
- 防止单链表是空的而设的,当链表为空的时候,带头结点的头指针就指向头结点,如果当链表为空的时候,单链表没有带头结点,那么它的头指针就为NULL。
- 是为了方便单链表的特殊操作,能有效减少代码量,在插入在表头或者删除第一个结点时不用考虑特殊情况,删除或插入用户的第一个节点的值和删除或插入中间的值用一样的代码,这样就保持了单链表操作的统一性!
- 单链表加上头结点之后,无论单链表是否为空,头指针始终指向头结点,因此空表和非空表的处理也统一了,方便了单链表的操作,也减少了程序的复杂性和出现bug的机会。
- 总结来说,没有头结点对第一个结点的操作大多和中间结点不太一样,每个操作都要考虑特殊情况,有头结点的话就不必考虑那么多了,还不容易出现代码错误。
代码
先贴上一些定义
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#define ERROR 0
#define OK 1
typedef int ElemType_Lk;
typedef int status;
typedef struct LNode{
ElemType_Lk data;
struct LNode* next;
}LNode,*LinkList;//结构体、结构体指针类型
查找元素
查找元素的思路:定义一个自由节点,用来遍历链表,并定义一个计数器,记录当前节点的逻辑顺序。当查找到第i个元素之前节点都不为空,就将该节点的数据返回。
查找元素的这种遍历方法是非常重要的,因为无论是插入指定位置元素、删除指定位置元素,其前提均是要遍历到该点。
status GetElem_Lk(LinkList L,int i,ElemType_Lk &e)
{
LinkList p; //结构体指针
int j = 0;//i为计数器
if (!L)return ERROR;
p = L; //p指向头结点
while (p && j<i)
{
j++;
p = p->next;
}
if (!p || j < i)return ERROR;
e = p->data;
return OK;
}
插入元素
插入元素的思路:首先要像查找元素那样遍历链表到要插入的位置的前一个节点,然后另外定义一个自由节点q,用来存放要插入的值,然后将节点q插入进去。
关于插入的顺序一定不能搞反了!举个形象的例子,看下面这个图,那男的是p,那妹子是p的下一个节点,那飞的是你(q)(是的,你会飞,掉不下去),现在要把你插入到他们中间。在你从p手里接过妹子的手之后,p才能松开抓住妹子的手赶紧抓住你。不然的话...试想,这哥们如果先抓住你的手,那就意味着他要松开妹子的手,妹子就掉了!掉了!啊,没了!等你想抓妹子的时候,妹子已经没影了。
此外一定要记得释放删掉的元素的内存。
status ListInsert_Lk(LinkList &L, int i, ElemType_Lk &e)//带头结点的链表的前插
{
LinkList p; //结构体指针,用来遍历链表
LinkList q; //新建一个结构体指针,用来插入链表
int j = 0;//i为计数器,头节点为0
if (!L)return ERROR;
p = L; //p指向头结点,
while (p && j < i-1)//因为要在前面插,所以要减1
{
j++;
p = p->next;
}
if (!p || j < i-1)return ERROR;
//q->data = e;//不好的写法,要动态申请内存
q = (LinkList)malloc(sizeof(LNode)); //申请内存
if (!q)exit(ERROR);
q->data = e;
q->next = p->next;
p->next = q;
//p->next = q;
//q->next = p->next; //错误写法,因为上一句已经改变了p->next
}
要切记,每个节点都要申请内存。申请内存一定要检查是否成功,否则有些编译器会警告。
删除元素
删除元素和插入元素很相似,只要将要被删除的节点的前面一个节点的指针绕过要删除的节点就行了。但要注意的是,被“绕过”的节点一定要释放掉内存!为了这样做,需要定义一个自由节点,来临时放删除的这个节点,并将其内存释放。
status ListDelet_Lk(LinkList& L, int i,ElemType_Lk &e)//带头结点的链表的删除,删除的元素放e中
{
LinkList p; //节点指针,用来遍历链表
LinkList q; //临时存放删除的节点去
int j = 0;//i为计数器
if (!L)return ERROR;
p = L; //p指向第一个节点
while (p && j < i-1)//删除第i个节点,需要将第i-1个节点的指针指向第i+1个节点
{
j++;
p = p->next;
}
if (!p || j < i-1)return ERROR;
//p->next = p->next->next;//可以这样写,但是有一个问题就是该内存没有释放,以后容易造成栈溢出
q = p->next;
p->next = q->next;
e = q->data;
free(q);//释放删掉的元素的内存
return OK;
}
后边关于单链表的创建方法(头插法、尾插法等)另外写。
这里有一个容易犯错的地方就是,插入和删除元素前,要遍历到哪?是遍历到要插入或要删除的那个节点的位置吗?不是的!是遍历到插入或被删除的节点的位置的前一个节点!因为无论是插入还是删除,都需要前一个节点进行操作。这也是为什么插入和删除的代码中遍历的位置不一样: