C语言链表
由于本人表较懒,加之你如果理解了单链表,那么双链表、循环链表我觉得都不是太大问题,所以本篇博文仅针对单链表进行详细讲解。整理匆忙,必有疏忽,敬请指正!
1. 基础知识
1.1 什么是链表 ?
链表是一种线性存储数据的结构,存储内容在逻辑上连续的,在物理上却不一定连续。其构成主要是:头指针(Header),若干个节点(节点包括了数据域和指针域),以及最后一个指向空的节点。
所以,实现原理很简单,头指针指向链表的第一个节点,然后第一个节点中的指针指向下一个节点,然后依次指到最后一个节点,这样就构成了一条链表。
1.2 链表的优点&缺点
优点:
- 插入删除速度快
- 内存利用率高,不会浪费内存
- 大小没有固定,拓展很灵活
缺点:
- 不能随机查找,必须从第一个开始遍历,查找效率低
1.3 复杂度
查找操作为O(n) ,插入和删除操作为O(1)。
2. 基本操作
2.1 链表的结构类型
链表内包含很多结点(当然也可以包含零个结点)。其中每个结点的数据空间一般会包含数据域用于存放各种类型的数据以及指针域,该指针一般称为next,用来指向下一个结点的位置。由于下一个结点也是链表类型,所以next的指针也要定义为链表类型。
typedef int ElemType;
typedef struct Listnode {
ElemType id;
struct Listnode *next;
}L;
2.2 创建一个节点并初始化
步骤:
- 创建一个节点,并为其分配空间大小;
- 将分配的空间进行惊醒清零;
- 初始化i数据与;
- 将指针域指向NULL;
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
L *Create_node(ElemType data)
{
L *node = NULL;
node = (L *)malloc(sizeof(L)); //给每个节点分配结构体一样的空间大小
if (node == NULL) {
printf("create node fail!\n");
return NULL;
}
memset(node, 0, sizeof(L)); //结构体未初始化,其中的内容为脏数据,因此要清零
node->id = data;
node->next = NULL; //将节点的后继指针设置为NULL
}
2.3 链表的头插法&尾插法
2.3.1 尾插法
先介绍尾插法,因为尾插比较简单,也更容易实现。为什么?在创建节点时,我们将节点的指针域指向了NULL,再想想链表最后一个节点是不是就需要指向NULL?所以,从"后面"进去,岂不是美滋滋? ? ?
步骤:
- 获取头指针,以访问链表;
- 判断是否为最后一个节点,如果不是则继续向一个节点移动,
如果是最后一个节点,则将节点插入;
//链表的尾插法
void tail_insert(L *List_header, L *new_node)
{
L *p = List_header; //获取头指针
while (NULL != p->next)
{
p = p->next; //只要当前位置的下一个节点不为空,则移动到下一个节点
}
p->next = new_node; //跳出循环,表示p已经是原链表的最后一个节点了,
//在其之后继续添加一个新节点,新节点new_node->next
//本身就指向了NULL
}
2.3.2 头插法
显然,尾插实现起来更方便,但是偶尔从前面,也是很爽的嘛[ps: 你不对劲!很不对劲!]言归正传,既然尾插很方便,那为什么还要有头插法?在应用时,如果你需要在创建节点时按某种方式排序,用尾插法需要在创建好链表后再进行排序。
在这里顺带提及一下头节点和头指针的区别:头指针就是链表的名字,仅仅是个指针而已,头指针始终指向链表的第一个结点,而头节点并不是所有链表都要求有的,如果有头结点,其内通常不存储数据信息,仅仅是为了操作的统一与方便而设立的;
插入一个新结点new_node时,必须先将new_node的next指向下个节点,再将前一个节点的next指针指向new_node,切记操作顺序不要改变。如果改变顺序,则会丢失后续节点的寻址方式。
代码如下:
//链表的头插法
void head_insert(L *List_header, L *new_node)
{
L *p = List_header;
new_node->next = p->next; //将new_node的next指向下个节点
p->next = new_node; //将前一个节点的next指针指向new_node
}
2.4 查找
2.4.1 按序号查找节点值
/*
从单链表的第一个节点出发,顺指针next域逐个往下搜索,
直到找到第i个节点为止,否则返回最后一个节点的指针域NULL
*/
L *GetElem(L *List_header, int i)
{
L *p = List_header->next; //p指向第一个节点
int j = 1; //用于计数
if (i == 0) //若i=0,则返回头节点
return List_header;
if (i < 0) //检查参数合法性
return NULL;
while(p != NULL && j < i) //从第一个节点开始查找,直至第i个节点
{
p = p->next;
j++;
}
return p; //返回第i个节点的指针(如果i大于链表长度,p指向了NULL,直接返回p即可)
}
2.4.2 按值查找链表节点
//按值查找链表节点
/*
从单链表的第一个节点开始,依次比较各节点中数据域的值,
若等于key值,则返回该节点的指针,若未寻找到该链表中
存在该key值,则返回NULL
*/
L *NodeNum(L *List_header, ElemType key)
{
L *p = List_header->next;
while (p->next != NULL && p->id != key)
{
p = p->next;
}
return p;
}
2.5 按给定序号插入
/*
首先通过GetElem函数查找到第num-1个节点,令新节点的next指针
指向原第num个节点,再将第num-1个节点的next指针指向新节点
*/
void insert_by_num(L *List_header, ElemType item, int num)
{
L *node = List_header;
L *newnode = (L *)malloc(sizeof(L));
newnode->id = item;
L *p = GetElem(List_header, num - 1);
newnode->next = p->next;
p->next = newnode;
}
2.6 删除
一般你在网上看到的博文都只有按序号删除节点,但实际应用中,更多的是按照给定值删除相应节点,所以我在这里提供这两个版本。
2.6.1 按序号删除节点
首先了了这个简单的,按照序号删除节点。
步骤:
- 寻找到给定序号的前一个节点p;
- 修改指针指向,将q->next指向q->next
- 别忘记free(q)
void Delete_by_num(L *List_header, int num)
{
L *p = GetElem(List_header, num - 1);
L *q = p->next;
p->next = q->next;
free(q);
}
2.6.2 按值删除节点
这种方式实现起来稍加复杂,主要是要考虑三种情况:
- 当删除节点是头节点时 ;
- 当节点是普通节点时;
- 当节点是尾节点(NULL)前一个时;
别急,我们step by step…
正常来说,我们在按值删除链表节点时需要遍历所有节点的数据域,一般来说,用一个while循环即可,但是考虑到要删除的节点可能包括头节点,所以对于头节点中的数据需要单独拿出来进行判断。那么在while循环中,需要考虑的就是第2、3两种情况,此时再进行一次判断,所以代码如下:
int Delete_by_key(L *List_header, ElemType key)
{
L *p = List_header;
while (p->next != NULL)
{
if (p->next->id == key) //寻找到要删除的值
{
L* temp = p->next;
if (p->next->next == NULL)
{
p->next = NULL;
free(temp);
}
else {
p->next = p->next->next;
free(temp);
}
}
else
{
p = p->next;
}
}
if (List_header->id == key) { //当删除的是头节点时,需要单独拿出来进行判断
L *temp = List_header;
List_header = List_header->next;
free(temp);
}
return 0;
}
嗯,暂时就写这么多吧,累了…