链表的基本概念:
链表作为数据结构的基础可以说是非常重要的,但是在开始之前我们在脑袋中应该知道链表其实是由不同的区域快链接而成,它不同于顺序表在内存中作为一个整体存储。
其次,我们称链表的每一个元素为一个结点,一个结点由数据域与指针域组成。指针域存在就是为了将散落分布在内存不同位置的节点联系起来,我们的一系列操作都基于此进行。
单链表有两种表示方式。分为有头结点和无头结点,二者并没有太多的区别,仅仅是作为表头作用。没有头结点时,我们的首元结点也就是第一个元素就是我们的头结点,链表命名也是基于该结点。本片文章主要讲解有头结点的链表。
结点的定义:
链表的结点本身不是一个简单的类型可以表示的,它由数据和指针组成,所以需要利用结构体将两个不同类型的变量组合成为一个整体。
//定义链表结构体类型
struct Lnode
{
int data;
struct Lnode* next;
};
链表的定义:
定义一个链表就是生成一个表头,也就是产生我们的头结点。头结点的结构与之后的结点一模一样,但是它的数据域对我们来说并不重要,所以可以置0,此时头结点后面并没有结点,那么它的指针域指向NULL,防止非法访问。
我们知道,要能够生成一片指定大小并且可以更改的空间需要用到malloc或者calloc这两个函数,我们的边表同样如此。对于开辟空间来说,我们还需要预防空间创建失败后的措施,这里用assert处理。
//定义一个链表
struct Lnode* Creat_New_List()
{
struct Lnode* Head_Node= (struct Lnode*)malloc(sizeof(struct Lnode));
if (Head_Node == NULL)
{
assert(Head_Node);
}
Head_Node->data = 0;
Head_Node->next = NULL;
return Head_Node;
}
创建一个结点:
创建一个结点的思想与创建我们的头结点相同,只不过多了一步数据赋值的操作。
//创建一个新的结点
struct Lnode* Creat_New_Node(int data)
{
struct Lnode* newNode = (struct Lnode*)malloc(sizeof(struct Lnode));
if (newNode == NULL)
{
assert(newNode);
}
newNode->data = data;
newNode->next = NULL;
return newNode;
}
尾插法插入结点:
尾插法顾名思义就是从头结点开始,不断地向后添加结点,直到我们指定的长度。中间用到的思想为当前结点的指针域等于我们新创建的结点地址,然后当前结点又指向下一个结点。
结点的指针域指向下一个结点之后,下一次的结点就是我们这一次被指向的结点了。因为需要不断地尾插,所以通过循环实现。
//尾插法
struct Lnode* Insert_End(struct Lnode* Head_Node, int cnt)
{
struct Lnode* New_Node = NULL;
struct Lnode* Pos_Node = Head_Node; //当前结点
while (cnt--)
{
New_Node = Creat_New_Node(0);
Pos_Node->next = New_Node;
Pos_Node = Pos_Node->next;
}
return Head_Node;
}
头插法插入结点:
头插法与尾插法正好相反,它每一次插入结点的位置都是我们头结点的后面,作为链表的第一个结点。我们就需要做到将插入结点的地址接到头结点的指针域,将插入节点的指针域指向上一个在头结点后面的那个结点的地址。在指针域的交接过程一定要注意不要让其中的某个空间失去了联系。例如,我们在将头结点的指针域改变只想之前,一定要提前用一个临时变量存储当前指针域的数据。不过要是先赋值我们插入结点的指针域就没有这个忧虑了。
//头插法
struct Lnode* Insert_Head(struct Lnode* Head_Node, int cnt)
{
int i = 0;
struct Lnode* Pos_Node = Head_Node;
struct Lnode* New_Node = NULL;
struct Lnode* Temp = NULL;
for (i = cnt; i > 0; i--)
{
New_Node = Creat_New_Node(0);
Temp = Pos_Node->next;
Pos_Node->next = New_Node;
New_Node->next = Temp;
}
return Head_Node;
}
任意位置插入一个结点:
随即插入一个结点的思想与我们的头插法类似,都需要让前一个结点指向本身,将自己的指针域指向原本该位置的结点。因为要插入的位置和数据由我们控制,那么函数形参就由下标,数据,加表头组成。
我们想要插入一个结点只需要利用循环遍历找到该结点的前面一个结点即可。其中,我们需要排除错误输入条件,比如小于1的插入值。
//插入结点
struct Lnode* Insert_Node(struct Lnode* Head_Node, int data, int index)
{
int i = 0;
struct Lnode* Pos_Node = Head_Node;
while (Pos_Node && i < index - 1) //找到第index-1个元素
{
i++;
Pos_Node = Pos_Node->next;
}
if (!Pos_Node || i>index - 1) return 0; //排除错误添加位置
struct Lnode* newNode = Creat_New_Node(data);
newNode->next = Pos_Node->next;
Pos_Node->next = newNode;
return Head_Node;
}
销毁链表:
整个链表是我们在堆空间创造出来的,所以在使用完之后需要有一个释放过程。释放的步骤为从前往后释放,每次释放只能释放一个,需要注意,释放之前需要将该节点的下一个空间存起来,以便后一个结点的释放。
//销毁链表
struct Lnode* Destroy_List(struct Lnode* Head_Node)
{
struct Lnode* Pos_Node = Head_Node; //指向表头
struct Lnode* temp_addr = NULL;
while (Pos_Node)
{
temp_addr = Pos_Node->next; //存储下一个结点
free(Pos_Node); //释放该结点
Pos_Node = temp_addr; //找到下一个结点
}
Head_Node = NULL;
return Head_Node;
}
以上就是关于链表最为重要的部分,剩下的都是我补充的一些关于链表数据的显示。
//查找数据下标
int Search_Index(struct Lnode* Head_Node, int data)
{
int j = 1;
struct Lnode* Pos_Node = Head_Node->next;
while (Pos_Node->data != data && Pos_Node)
{
Pos_Node = Pos_Node->next; //向后遍历,直到找到或者结束
j++;
}
if (Pos_Node = NULL) return -1; //没找到
else return j; //找到返回下标
}
//求链表长度
int Get_List_Length(struct Lnode* Head_Node)
{
int i = 0;
struct Lnode* Pos_Node = Head_Node->next; //指向第一个结点
while (Pos_Node)
{
Pos_Node = Pos_Node->next;
i++;
}
return i;
}
//打印单个结点内容
int Get_List_Data(struct Lnode* Head_Node, int index)
{
int i = 1;
struct Lnode* Pos_Node = Head_Node->next;
while (Pos_Node && i < index)
{
Pos_Node = Pos_Node->next;
i++;
}
if (i>index) return 0;
return Pos_Node->data;
}
以上就是我对单链表操作的全部理解了,希望能够帮到你 ^ _ ^