原创 小白视角下解读TencentOS tiny 预备篇(数据结构之单链表的操作)

什么是单链表

	单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) +指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。

话不多说,看图
单链表图示
说白了就是一个结构体里边有一个变量,和一个本身结构体的指针;指针指向下一个结构体,就这样指着指着就成了一条链。这就是单链表
上图的结构体定义如下,数据域有两个变量,方便查看我直接用data代替这两个成员;

typedef struct _QUEUE_CB_
{	
	 int  data;     //数据域
	 char name;    //数据域
	 struct _QUEUE_CB_* next;		//指针域  这里是定义了一个结构体指针
	
}Queue_cb;  //这个只是上面的别名

创建一个单链表头

//创建一个链表表头
Queue_cb *Create()
{
	Queue_cb *head;  //声明一个表头	
	head = (Queue_cb *)malloc(sizeof(Queue_cb));  //申请一个动态内存,内存大小为这个Queue的长度,
	//申请之后就要对他进行初始化
	head -> next = NULL;
	return head;  //返回表头指针
}

理解起来也不难,声明一个Queue_cb 类型的指针,用来指向malloc申请的地址,地址的大小就是这个结构体的大小,申请出来后就初始化指针为空即可,数据可以先不初始化,形成差异化,方便后面更改数据类型的时候进行操作。
图示:
链表表头

新建一个节点

//创建节点 节点是有参数传入的,传入的参数就是结构体的成员变量
Queue_cb *Node(char Name, int Age)
{
	Queue_cb *node;    //定义一个Queue结构体指针,作为新节点的指针
	
	node = (Queue_cb*)malloc(sizeof(Queue_cb));  //这个指针指向Queue结构体的申请的内存
	
	node -> name = Name;    //初始化赋值
	
	node -> age = Age;		//初始化赋值
	
	node -> next = NULL;
	
	return node;   //返回这个节点地址

}

相比上面的创建表头,没什么区别吧,只是多了数据域的传入而已,很简单吧,看图,数据域初始化了变量为函数传入的参数,指针照样指向空
node节点

链表的插入,拼接

上面介绍了表头的创建和节点的创建,那么接下来就是如何形成由点变成线的过程;怎么操作呢?很简单,把表头的指针指向新建立的节点地址就可以啦;那对于指向的节点,我想先建立的节点在表头的后边,后面的节点依次排队,又或者我想后面建立的节点放在表头后边;这就引入了一个节点的插入法,常见的插入法有头插法、尾插法、指定位置插法

  • 头插法:就是建立的节点一直往表头后边插,形象的比喻就是 排队领饭的时候,每个人都想先吃,后面来的直接插到第一个位置,先来到的只能往后站;还是看代码吧;
//链表的头插法插入
void NodeInsertHead(Queue_cb *head, char name, int age)
{
	Queue_cb *InsertNode;    //定义一个插入的节点结构体指针
	
	InsertNode = Node(name,age);  //创建这个结构体指针的内存,并初始化 
	
	InsertNode -> next = head -> next;  //把当前的节点插入头结点后边
	
	head -> next = InsertNode;  //头结点的下一个就是当前的这个节点
	
}

可能看的不是很明白,我画个图讲解一下
头插法
Queue_cb *head 是你建立的头结点指针,传入你的头结点地址
先去掉中间的newNode,和与他相连的红色的箭头,当建立第一个节点的时候,表头的next指针就指向你建立的这个节点Node,也就是灰色的箭头,这就对应了代码 head -> next = InsertNode; //头结点的下一个就是当前的这个节点
InsertNode -> next = head -> next; //这里是第一个节点,头初始化的时候next是指向NULL的,所以这个节点的next指针代替头的next指针,指向空,这样就连接了头节点和第一个节点;
现在又来了一个新节点newNode,图示对应红色的箭头,把灰色的去掉,(不懂的最好自己画一下,我是偷懒直接把两个放在一起了:))
代码解释也跟上面一样:
head -> next = InsertNode 表头的next指针就指向你建立的这个新节点Node
InsertNode -> next = head -> next; 这个节点的next指针代替头的next指针,在上边第一个节点的时候
head -> next是指向第一个节点的,所以这里是直接代替他来指向第一个节点,这样头插法就成功了
看下实验现象把:

int main()
{
	delay_init();
	/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
	uart_init(115200);
	LED_Init();
	printf("Init successfully!\r\n");
	
	Queue_cb *Nodelist = Create();  //创建链表表头
	
	NodeInsertHead(Nodelist,'a',10);		//头插法插入链表
	NodeInsertHead(Nodelist,'d',11);
	NodeInsertHead(Nodelist,'w',15);
	NodeInsertHead(Nodelist,'G',20);
	NodeInsertHead(Nodelist,'h',13);
	NodeInsertHead(Nodelist,'j',44);
	Printf_list(Nodelist); //打印链表        	
	while(1)
	{	
	}
}

现象:
图示
看,是不是最后插进去的排在第一位;这就是头插法,理解了头插法,后面的就不难了。

  • 尾插法: 就是依次排队下去,先遍历到最后一个,然后再插入。
    `//链表的尾插法插入
    void NodeInsertTail(Queue_cb *tail, char name , int age)
    {
    Queue_cb *Insertnode; //创建的节点指针

    while(tail->next != NULL)   //遍历到当前的链表的表尾
      {				
      		tail = tail->next;		//因为最后一个的next是指向NULL的,所以会一直遍历到最后一个
      }
      
      Insertnode = Node(name,age);  //指针指向开辟的空间,并初始化
      			
      Insertnode -> next = NULL;		//尾节点的下一个节点是空
      
      tail->next = Insertnode;		 // 链表尾的下一个节点指向当前创建的节点
    

}
`
看现象

	//在main函数中添加以下 
	NodeInsertTail(Nodelist,'O',50);   //尾插法
	NodeInsertTail(Nodelist,'P',70);
	NodeInsertTail(Nodelist,'Q',90);

尾插法
对比头插法,尾插法就是按顺序排列下去

  • 指定位置插入: 思路是遍历链表的长度,然后如果插入的位置在长度之内,直接插入
int NodeInsert_Pos(Queue_cb *list, u8 pos, char name, int age)
{
		u8 list_lenth,i = 1;
		Queue_cb *Insertnode;   //要插入的节点	
	
		list_lenth = Traves_lenth(list);   //获取要插入的链表的长度
		
		if(pos > list_lenth)    //判断插入的位置是否非法
		{				
				return -1;			
		}
		
		if(pos == list_lenth)   //如果插入的位置是这个链表的长度,那就是在尾巴插入
		{
				NodeInsertTail(list, name, age);
			 return 2;
		}
		
		while((list->next != NULL) && (i < (pos))) //遍历链表,遍历到指定位置的上一个节点
		{
		
				list = list->next;		  	
				++i;
		}
		Insertnode = (Queue_cb *)malloc(sizeof(Queue_cb));   //开辟一个地址,给你要插入的节点放数据
			
		Insertnode -> next = list -> next;  //把原来链表本来要指向的下一个节点,让插入的节点来代替它指向	
		list -> next = Insertnode;		//把链表要指向的节点指向这个插入的节点	
		
		Insertnode -> age = age;				//初始化数据	
		Insertnode -> name = name;
		
		return 0;
}

//遍历链表长度代码

u8 Traves_lenth(Queue_cb *list)
{
		u8 Lenth = 0;
		
		while(list->next != NULL)
		{
			list = list->next;   //把下一个节点的地址赋给当前的地址,让它遍历下去
			Lenth++;		
		}	         //当list->next = NULL的时候证明已经是最后一个节点了,所以知道了这个链表的长度
		return Lenth;
}

现象:
主函数添加
NodeInsert_Pos(Nodelist,5,‘X’,77); //增 在第5个位置插入X
增
不知道你们看不看得出我这个代码里边有个小bug,就是没有处理插入位置大于链表的长度的这种情况

链表的删除

有创建有会有删除,来看看删除操作是怎么样子的;
删除节点
诶?这图怎么似曾相识?没错就是跟头插法一样的,只不过这个是反过来操作而已,第一步删除灰色的线,只留下红色的,这就是正常的链表,这个要删除的节点先叫A点,A点的上一个节点的next是指向A点的,A点的next是指向A点的下一个,所以删除操作就是把A点的上一个节点的next指向A点的下一个节点,直接跳过A,就行了,第二步就是 删除了之后就是去掉红色的箭头和中间的节点,保留灰色箭头的样子(原谅我懒)
还有一个就是怎么知道我要删除哪个节点呢?,通过遍历。遍历到你要删除节点的上一个,再执行上面的操作就行
一定要注意释放空间,一定要释放空间,一定要释放空间,一定要记得调用free()来释放你申请的空间至于为什么,后面讲到堆栈再说
看代码:

void  Delect_node(Queue_cb *list, u8 index)
{
		Queue_cb *p,*f;
		u8 i = 1,lenth;
	
		p = list;
		if(p == NULL)     //判断链表是否为空
		{
				printf("Error , list is empty\r\n");	
		}	
		lenth = Traves_lenth(list); //获取链表的长度
		if(index > lenth)    //判断要删除的节点是否在链表内
		{
				printf("Index is not in list\r\n");
		}		
		while((p -> next != NULL) && (i < index))  //遍历到指定的位置的上一个
		{
				p = p -> next;
				i++;      
		}
		
		printf("已删除的节点内容 : %d  name: %c  age: %d \r\n",index,p->next->name, p->next->age);		
		//p是你删除节点的上一个节点,p->next就是你要删除的节点
		f = p->next;	 //f指向的就是要删除的节点的地址,把f->next指向的地址代替f
		p->next = p->next->next;	//	p的下一节点就指向 你要删除的节点(p->next)的下一个节点也就是 p->nex->next		
		free(f);  //释放该空间	 
}

主函数添加  Delect_node(Nodelist,5);  //删

现象
在这里插入图片描述

链表的查找

链表的查找也很简单,就是遍历到你要查找的位置就行了

void Search_list(Queue_cb *list, u8 Index)
{
		Queue_cb *p;
		u8 i, lenth;
		p = list;
						
		if(p == NULL)
		{
				printf("Error , list is empty\r\n");	
		}
		
		lenth = Traves_lenth(list); //先获取链表的长度
		if(Index > = lenth)  //查看位置是否合法,此处不包括尾
		{
				printf("Index is not in list\r\n");
		
		}		
		while((p -> next != NULL) && (i < Index)) //查找的依据是,你找的那个点在头和尾之间,index是你要找的位置。i是遍历了的节点,i不是你要找的节点就一直加
		{
				p = p -> next;
				++i;
		}
		printf("查找的节点:%d  name : %c  age : %d \r\n",Index,p->name,p->age);
}```

现象:
主函数加入

int main()
{
delay_init();
/初始化USART 配置模式为 115200 8-N-1,中断接收/
uart_init(115200);
LED_Init();
printf(“Init successfully!\r\n”);

Queue_cb *Nodelist = Create();  //创建链表表头

NodeInsertHead(Nodelist,'a',10);		//头插法插入链表
NodeInsertHead(Nodelist,'d',11);
NodeInsertHead(Nodelist,'w',15);
NodeInsertHead(Nodelist,'G',20);
NodeInsertHead(Nodelist,'h',13);
NodeInsertHead(Nodelist,'j',44);
	
NodeInsertTail(Nodelist,'O',50);   //尾插法
NodeInsertTail(Nodelist,'P',70);
NodeInsertTail(Nodelist,'Q',90);	

NodeInsert_Pos(Nodelist,5,‘X’,77); //增
Search_list(Nodelist,8); //找到第8个节点
Printf_list(Nodelist); //打印链表
while(1)
{
}
}

放不了图片了,我把现象发出来吧
打印的是:

[22:54:49.315]收←◆Init successfully!
查找的节点:8  name : \0  age : 0 
j,44
h,13
G,20
w,15
X,77
d,11
a,10
O,50
P,70
Q,90

更改链表的节点

改节点那就简单啦,直接定位到你要改的节点,然后传入你要改的数据,OJBK

/*
*		更改链表中指定节点的内容
*		输入 需要更改的链表  要更改的位置,更改的内容
*/

void Change_Node(Queue_cb *list, u8 index, char name, int age)
{
		Queue_cb *p;
		u8 i = 1, lenth;
	
		p = list;
						
		if(p == NULL)     //判断链表是否为空
		{
				printf("Error , list is empty\r\n");	
		}
		
		lenth = Traves_lenth(list); //先获取链表的长度
		if(index > lenth)
		{
				printf("Index is not in list\r\n");
		
		}		
		while((p -> next != NULL) && (i < (index)))  //遍历到指定位置的上一个
		{
				p = p -> next;
				++i;
		}
		
		printf("更改前的节点:%d  name : %c  age : %d \r\n",index,p->next->name,p->next->age);
		
		p->next->age = age;
		
		p->next->name = name;		
		
		printf("更改后的节点:%d  name : %c  age : %d \r\n",index,p->next->name,p->next->age);

}


主函数 加入
Change_Node(Nodelist,5,‘L’,88); //改
现象:

[23:01:38.360]收←◆Init successfully!
更改前的节点:5  name : X  age : 77 
更改后的节点:5  name : L  age : 88 
j,44
h,13
G,20
w,15
L,88
d,11
a,10
O,50
P,70
Q,90

怎么样,简单吧

最后

虽然我知道我自己的C基础不是很扎实,比不上人家大佬,但我会一点一点的积累,慢慢的提升自己。
也希望有人能指出我的错误,让我及时改正。
下一篇:双向链表操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值