复习笔记1:C语言的指针和通用双向循环链表学习记录

概要

本人正在准备24年秋招,现记录从现在开始的复习笔记。
本节是笔记1:C语言的指针通用双向循环链表学习记录。

指针

野指针

  • 野指针是指指向不可用内存的指针。当指针被创建时,指针不可能自动指向 NULL,这时默认值是随机的,此时指针成为野指针。
  • 当指针被 free 或 delete 释放掉时,如果没有将指针指向 NULL,就会产生野指针,因为释放掉的是指针指向的内存,没有将指针本身释放掉。造成野指针的原因也可能是指针操作超越了变量的作用范围。比如数组越界,避免野指针的方法即使使用完之后释放内存,并将指针赋 NULL。

空指针

  • 空指针是指没有指向任何一个存储单元的指针。

通用双向循环链表

通用双向循环链表可以包含单链表、通用链表、双向链表、循环链表的全部知识,所以只记录学习通用双向循环链表即可。

通用性

  • 指链表中结点包含数据域和指针域,而通用链表的指针域通常用通用的node指针来表示,与该链表本身类型关系不大,只需要包含通用指针域即可。如:(node为通用指针域)
//学生结构体
typedef struct student{
    int num;
    char name[20];
    float score;                    
    NODE node;      //通用指针域类型的变量
}STU,*PSTU;

双向性

  • 指针域包含前向指针pre后向指针next
//**************指针域************//
//该结构体是专门的指针域,包含在每个节点之中
typedef struct node_t{
    struct node_t *pre;              //前向指针
    struct node_t *next;             //后向指针
}NODE,*PNODE;

循环性

  • 链表尾部结点的next需要指向链表头,而非NULL,形成一个循环,后续初始化链表会有代码体现。

新建和初始化链表

  • 初始化链表就是构造链表头结构体,分配内存。
//******链表********//
/* 构造链表头结构体 */
typedef struct List{
    char *name;
    NODE head;
}LIST,*PLIST;
PLIST stu_list;	//新建链表

可以在main函数中直接新建链表让编译器自动为我们分配内存

/********************************************************
* Description   : 新建一个链表头部
* @List         :已分配好内存的链表
* Return        :无
**********************************************************/
void NewListInit(PLIST List)
{
    List->head.next    = &List->head;  //头部next属性设置为自己
    List->head.pre     = &List->head;  //头部pre属性设置为自己
}

在这里我们可以发现,在建立循环链表的时候使需要把pre和next属性都指向自己的地址,形成循环。

尾插法插入结点

  • 获取链表尾部,此时链表里面只有头结点,所以头结点作为我们的尾部
  • 将被添加结点的next属性指向尾部结点的next属性
  • 将尾部结点的next属性指向被添加结点地址
  • 将被添加结点的pre属性指向尾部结点
  • 将此时被添加结点的next属性指向的结点的pre属性指向被添加结点。
/********************************************************
* Description   : 添加一个节点至链表尾部
* @pList        :需要添加节点的链表指针
* @new_node     :被添加的节点指针
* Return        :无
**********************************************************/
void AddItemToList(PLIST pList, PNODE new_node)
{
	PNODE last = pList->head.pre;  //找到链表尾部节点
	
	new_node->next  = last->next; //设置新添加节点next属性
	last->next      = new_node;   //设置尾部(当前)节点next属性
	new_node->pre   = last;       //设置新添加节点pre属性
	pList->head.pre = new_node;   //设置新添加节点的next节点pre属性
}

链表中间插入结点

与尾插法的方法一致,只是这里知道被插入结点是位于哪个结点后面,就不需要获取尾部结点了。

/********************************************************
* Description   : 插入一个节点至目标节点后面
* @Target       :目标节点指针
* @new_node     :被添加的节点指针
* Return        :无
**********************************************************/
void AddItemAfter(PNODE Target, PNODE new_node)
{
    new_node->next      = Target->next; //设置新添加节点next属性
    Target->next        = new_node;     //设置目标节点next属性
    new_node->next->pre = new_node;     //设置新添加节点的next节点pre属性
    new_node->pre       = Target;       //设置新添加节节点pre属性
}

删除结点

  • 被删除结点左侧结点next属性指向右侧结点
  • 被删除结点右侧结点pre属性指向左侧结点
/********************************************************
* Description   : 删除一个节点
* @del_node     :被删除的节点指针
* Return        :无
**********************************************************/
void DelItemFromList(PNODE del_node)
{
    del_node->next->pre     = del_node->pre;
    del_node->pre->next     = del_node->next;
}

反推数据域

根据指针域反推数据域是通用链表的核心知识,通用链表只通过node指针域进行连接,每个不同类型的链表都有自己的数据域格式,我们需要通过node反推出数据域的地址才能访问到里面的变量

学生链表:

//学生结构体
typedef struct student{
    int num;
    char name[20];
    float score;                    
    NODE node;      //通用指针域类型的变量
}STU,*PSTU;
(struct student *)((char *)p - (unsigned int)&((struct student *)0)->node)
  • 我们来分析这行代码,首先是最终值,它是一个 (struct student *)指针,我们的目的就是获取这样一个指针,使他可以访问到student学生结构体的所有信息,如num、name
  • 我们所拥有的是node结点的地址,因为这可以从链表中获取到。
  • 这行代码中的p就是我们从链表中获取的node结点地址
  • 内存中结构体地址的递增的,所以我们只要获取到student结构体首地址到node属性的地址处递增了多少字节,然后我们再用p减去递增的字节数就可以得到student的首地址了。
  • (struct student *)0)->node这行代码是利用0地址下使用struct student *去访问node属性,为什么是0地址,因为我们把这个指针设置成0地址之后此时node属性的地址就正好是student 结构体中储存node属性递增的字节数了。
  • (unsigned int)&((struct student *)0)->node可看到中间有个取址符号,此时它的地址就是递增字节数,我们把他强转成(unsigned int)告诉编译器进行普通的指针加减运算。
  • (char *)p这里要强转char *告诉编译器进行普通指针加减运算。
  • (struct student *)最后强转成我们想要的student 首地址,接下来就可以通过这个指针访问里边的变量了。

链表排序

使用冒泡排序法,做两层排序,四步走

  • 两个结点,一个比较结点,一个被比较结点。比较结点是外循环,被比较结点内循环
  • 外循环的比较结点固定,然后内循环开始递增,逐个比较。
  • 比较函数后面讲,先关注排序逻辑,我们从小到大排序,比较函数返回小于时不交换,比较函数返回大于时进行交换
  • 结点交换的实现
    • 记录下两个结点的前一个结点地址
    • 将两个结点都删除
    • 结点2插入结点1前一个结点之后,结点1插入结点2前一个结点之后
    • 还需要判断两个结点相邻的情况,相邻情况下需要特殊处理,删除结点之后直接将结点2插入结点1之后即可
void SortList(PLIST pList)
{
	struct node_t *pre1 = &pList->head;	//从链表头开始
	struct node_t *pre2;				//这个是用来记录上一次比较时被比较数的pre指针
	struct node_t *cur = pre1->next;	//开始比较的第一个结点
	struct node_t *next;				//类比数组的索引,冒泡排序的i,j
	struct node_t *tmp;					//用于交换的中间结点
		
	while (cur != &pList->head) //当前比较的目标不是链表尾部
	{
		pre2 = cur;
		next = cur->next;
		while (next != &pList->head)    //冒泡排序第二层
		{
			if (CmpStudentNum(cur, next) == 0)
			{
				/* 交换节点 */
				/* 1. del cur */
				DelItemFromList(cur);
				/* 2. del next */
				DelItemFromList(next);
				/* 3. 在pre1之后插入next */
				AddItemAfter(pre1, next);   //pre1记录的是比较数的pre指针
				/* 4. 在pre2之后插入cur */
				if (pre2 == cur)            //如果被比较数pre指针等于当前节点的指针,说明两个节点相邻
					AddItemAfter(next, cur);
				else
					AddItemAfter(pre2, cur);
				
				/* 5. cur/next指针互换 */
				tmp = cur;
				cur = next;
				next = tmp;				
			}
			
			pre2 = next;            //这个是用来记录上一次比较时被比较数的pre指针
			next = next->next;      //继续往前比较
		}
		
		pre1 = cur;         //这个是用来记录上一次比较时比较数的pre指针
		cur = cur->next;    //继续往前比较
	}
}
  • 比较函数
#define container_of(ptr, type, member) \
	(type *)((char *)ptr - (unsigned int)&((type *)0)->member)

宏定义实现前面说的反推数据域操作,实现原理是一样的。

/********************************************************
* Description   : 比较两个学生的num属性
* @pre          :目标节点1
* @next         :目标节点2
* Return        :大于时返回0, 小于时返回-1
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next)
{
	PSTU p;
	PSTU n;
	
	p = container_of(pre, struct student, node);
	n = container_of(next, struct student, node);
	
	if (p->num < n->num)
		return -1;
	else
		return 0;
}

代码很简单,反推出数据域之后比较他们的num参数,根据结果进行返回即可。

完整工程代码

完整工程代码如下,已经过验证,可以正常运行。

#include "stdio.h"
#include "stdlib.h"

#define container_of(ptr, type, member) \
	(type *)((char *)ptr - (unsigned int)&((type *)0)->member)

//**************指针域************//
//该结构体是专门的指针域,包含在每个节点之中
typedef struct node_t{
    struct node_t *pre;              //前向指针
    struct node_t *next;             //后向指针
}NODE,*PNODE;

//******链表********//
typedef struct List{
    char *name;
    NODE head;
}LIST,*PLIST;

//学生结构体
typedef struct student{
    int num;
    char name[20];
    float score;                    
    NODE node;      //通用指针域类型的变量
}STU,*PSTU;

typedef struct teacher              //教师
{
    int num;
    char name[20];
    char subj[20];                  
    NODE node;                     //同上
}TEA,*PTEA;

/********************************************************
* Description   : 新建一个链表头部
* @List         :已分配好内存的链表
* Return        :无
**********************************************************/
void NewListInit(PLIST List)
{
    List->head.next    = &List->head;  //头部next属性设置为自己
    List->head.pre     = &List->head;  //头部pre属性设置为自己
}

/********************************************************
* Description   : 添加一个节点至链表尾部
* @pList        :需要添加节点的链表指针
* @new_node     :被添加的节点指针
* Return        :无
**********************************************************/
void AddItemToList(PLIST pList, PNODE new_node)
{
	PNODE last = pList->head.pre;  //找到链表尾部节点
	
	new_node->next  = last->next; //设置新添加节点next属性
	last->next      = new_node;   //设置尾部(当前)节点next属性
	new_node->pre   = last;       //设置新添加节点pre属性
	pList->head.pre = new_node;   //设置新添加节点的next节点pre属性
}

/********************************************************
* Description   : 插入一个节点至目标节点后面
* @Target       :目标节点指针
* @new_node     :被添加的节点指针
* Return        :无
**********************************************************/
void AddItemAfter(PNODE Target, PNODE new_node)
{
    new_node->next      = Target->next; //设置新添加节点next属性
    Target->next        = new_node;     //设置目标节点next属性
    new_node->next->pre = new_node;     //设置新添加节点的next节点pre属性
    new_node->pre       = Target;       //设置新添加节节点pre属性
}

/********************************************************
* Description   : 删除一个节点
* @del_node     :被删除的节点指针
* Return        :无
**********************************************************/
void DelItemFromList(PNODE del_node)
{
    del_node->next->pre     = del_node->pre;
    del_node->pre->next     = del_node->next;
}

/********************************************************
* Description   : 链表排序
* @pList        :目标链表
* Return        :无
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next);
void SortList(PLIST pList)
{
	struct node_t *pre1 = &pList->head;	//从链表头开始
	struct node_t *pre2;				//这个是用来记录上一次比较时被比较数的pre指针
	struct node_t *cur = pre1->next;	//开始比较的第一个结点
	struct node_t *next;				//类比数组的索引,冒泡排序的i,j
	struct node_t *tmp;					//用于交换的中间结点
		
	while (cur != &pList->head) //当前比较的目标不是链表尾部
	{
		pre2 = cur;
		next = cur->next;
		while (next != &pList->head)    //冒泡排序第二层
		{
			if (CmpStudentNum(cur, next) == 0)
			{
				/* 交换节点 */
				/* 1. del cur */
				DelItemFromList(cur);
				/* 2. del next */
				DelItemFromList(next);
				/* 3. 在pre1之后插入next */
				AddItemAfter(pre1, next);   //pre1记录的是比较数的pre指针
				/* 4. 在pre2之后插入cur */
				if (pre2 == cur)            //如果被比较数pre指针等于当前节点的指针,说明两个节点相邻
					AddItemAfter(next, cur);
				else
					AddItemAfter(pre2, cur);
				
				/* 5. cur/next指针互换 */
				tmp = cur;
				cur = next;
				next = tmp;				
			}
			
			pre2 = next;            //这个是用来记录上一次比较时被比较数的pre指针
			next = next->next;      //继续往前比较
		}
		
		pre1 = cur;         //这个是用来记录上一次比较时比较数的pre指针
		cur = cur->next;    //继续往前比较
	}
}


/********************************************************
* Description   : 比较两个学生的num属性
* @pre          :目标节点1
* @next         :目标节点2
* Return        :大于时返回0, 小于时返回-1
**********************************************************/
int CmpStudentNum(PNODE pre, PNODE next)
{
	PSTU p;
	PSTU n;
	
	p = container_of(pre, struct student, node);
	n = container_of(next, struct student, node);
	
	if (p->num < n->num)
		return -1;
	else
		return 0;
}

/********************************************************
* Description   : 打印链表
* @pList        :目标链表
* Return        :无
**********************************************************/
void PrintList(PLIST pList)
{
	int i = 0;
	
	PNODE node_1 = pList->head.next;      //得到链表头部的next属性
	PSTU p;
	
	while (node_1 != &pList->head)        //如果头部的next不是头部,代表还没有到链表尾部
	{
		p = container_of(node_1, struct student, node);
		printf("person %d: num is %d\r\n", i++, p->num);
		
		/* 后面还有人, 移动到下一个 */
		node_1 = node_1->next;
	}
}

int main(int argc, char *argv[])
{
    PLIST stu_list;
    PSTU stus1,stus2,stus3,stus4,stus5,stus6;

    stu_list = (PLIST)malloc(sizeof(LIST)); //分配内存
    NewListInit(stu_list);                  //初始化链表

    stus1 = (PSTU)malloc(sizeof(STU)); //分配内存
    stus2 = (PSTU)malloc(sizeof(STU)); //分配内存
    stus3 = (PSTU)malloc(sizeof(STU)); //分配内存
    stus4 = (PSTU)malloc(sizeof(STU)); //分配内存
    stus5 = (PSTU)malloc(sizeof(STU)); //分配内存
    stus6 = (PSTU)malloc(sizeof(STU)); //分配内存

    stus1->num = 20;
    stus2->num = 40;
    stus3->num = 30;
    stus4->num = 10;
    stus5->num = 50;
    stus6->num = 100;

    AddItemToList(stu_list, &stus1->node);  //添加进链表尾部,尾插法
    AddItemToList(stu_list, &stus2->node);  //添加进链表尾部,尾插法
    AddItemToList(stu_list, &stus3->node);  //添加进链表尾部,尾插法
    AddItemToList(stu_list, &stus4->node);  //添加进链表尾部,尾插法
    AddItemToList(stu_list, &stus5->node);  //添加进链表尾部,尾插法

    PrintList(stu_list);

    printf("\r\n Del node3\r\n");
    DelItemFromList(&stus3->node);
    PrintList(stu_list);

    printf("\r\n add node1->node6\r\n");
    AddItemAfter(&stus1->node, &stus6->node);
    PrintList(stu_list);

    printf("\r\n Sort List\r\n");
    SortList(stu_list);
    PrintList(stu_list);

    printf("yes!\r\n");
}
  • 39
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值