《C和指针》阅读笔记(11)---单链表数据结构的设计和实现

前面的10篇文章总体上过了一遍c语言的基础,这些基础是非常重要的基石。有助于我们理解下一个阶段的内容,我们常说程序=数据结构+算法,本文将介绍一种常用的数据结构,链表。给出的示例代码具备复用性,如果你不关心内功的修炼,基本上可以拿来主义(白嫖);如果想提升内功,不妨仔细阅读代码。

什么是链表

链表(linked list)就是一串节点组成的集合,每一个节点就是一个独立的数据结构。可以将链表想象成一串珍珠,整串珍珠就相当于一个链表,每个珍珠就可看成是一个节点。

链表的类型有单链表和双链表,如果链表头尾相接那就是循环链表。链表的实现大体上可以分为两种,一种是基于数组,一种是基于动态节点,具体的实现细节也各不相同。每种类型的链表我会分享两种实现方式,第一种方式相对直观容易理解,第二种方式使用更多的技巧(下篇文章分享),使通用性更强一些。

单链表

重点在于吃透代码,如果理解上有困难,不妨拿纸笔画一画。或者评论留言。

方式一

先给出头文件link.h,介绍数据结构和几个API。结构体成员的作用以及API的作用和接口说明,已经非常详细的进行了注释。这种方式也是工程中常用的规范化的编码风格。

#ifndef _LINK_H
#define _LINK_H

#ifdef __cplusplus
extern "C"
{
#endif

#include <stdio.h>
#include <stdlib.h>

/**
 * @brief 链表节点数据结构
 */
typedef struct _node
{
	int             data;   // 节点的值
	struct _node    *next;  // 指向下一个节点的指针
}node_t;


/**
 * @brief head_append 
 *      向链表头中插入一个新的节点
 *
 * @param[in/out] header
 *  链表头指针
 * @param[in] val
 *  新节点的值
 *
 * @return
 *  @val 0, 成功
 *  @val 1, 失败
 */
int head_append(node_t **header, int val);

/**
 * @brief tail_append 
 *      向链表尾中插入一个新的节点
 *
 * @param[in/out] header
 *  链表头指针
 * @param[in] val
 *  新节点的值
 *
 * @return 
 *  @val 0, 成功
 *  @val 1, 失败
 */
int tail_append(node_t **header, int val);

/**
 * @brief del_link 
 *      删除链表
 *
 * @param[in/out] header
 */
void del_link(node_t** header);

/**
 * @brief find_mid 
 *      查找链表的中间节点
 *
 * @param[in] header
 *  链表头指针
 *
 * @return 找到,则返回该中间节点的指针;否则,返回NULL
 */
node_t* find_mid(node_t** header);


/**
 * @brief turnover_link 
 *      翻转链表
 *
 * @param[in/out] header
 */
void turnover_link(node_t** header);

/**
 * @brief del_node 
 *      删除链表中值为val的所有节点
 *
 * @param[in/out] header
 * @param[in] val
 */
void del_node(node_t** header, int val);


/**
 * @brief print_link 
 *      打印链表
 *
 * @param[in] header
 */
void print_link(node_t *header);

#ifdef __cplusplus
}
#endif
#endif //_LINK_H

再来看看每个API在link.c中的具体实现。当然,每个函数的实现也进行了详细的注释。

#include "link.h"

// 头插,每个新节点都往链表头不插入
int head_append(node_t **header, int val)
{
    // 创建新节点
	node_t *new = malloc(sizeof(node_t));
	if (NULL == new)
	{
		printf("malloc new node failed...\n");	
		return 1;
	}

    // 新节点的next指针指向链表的头节点
	new->next = (*header);
	new->data = val;
    // 头指针指向新的节点
	*header = new;

    return 0;
}

// 尾插, 每个新节点都往链表尾部插入
int tail_append(node_t **header, int val)
{
	node_t** tmp = header;
	node_t* cur = NULL;

	node_t *new = malloc(sizeof(node_t));
	if (NULL == new)
	{
		printf("malloc new node failed...\n");	
		return 1;
	}

	new->data = val;

    // 从前往后遍历找到最后一个节点
	while(cur = *tmp)
		tmp = &(cur->next);

	new->next = *tmp;
	*tmp = new;

    return 0;
}


// 删除链表
// 从前往后遍历删除每一个节点
void del_link(node_t** header)
{
	node_t *tmp = *header;
	
	while(*header)
	{
        // 指向下一个节点
		*header = (*header)->next;	
        // 释放头节点
		free(tmp);
        // 暂存下一个节点
		tmp = *header;
	}
}

// 查找中间节点
// 通过快慢指针查找中间节点
node_t* find_mid(node_t** header)
{
	node_t* slow = *header;
	node_t* fast = *header;

	if (NULL == *header)
	{
		printf("empty link...\n");
		return NULL;
	}

	while(fast->next)
	{
		slow = slow->next;	
		fast = fast->next;
		if(fast->next != NULL)
			fast = fast->next;
	}

	return slow;
}

// 翻转链表
// 通过两个指针保持旧链表的连接
// header总是指向新链表的头节点
void turnover_link(node_t** header)
{
	node_t* tmp = *header; // tmp指向头节点
	node_t* cur = NULL; // cur指针用来指向旧链表
	*header = NULL; // header指向NULL

	while(cur = tmp) // cur 指向待被转移的节点
	{
		tmp = tmp->next; // tmp指向待被转移的节点的下一个节点,保证不丢失旧链表
		cur->next = *header; // 待被转移的节点指向新链表的头节点,待被转移节点与新链表建立联系
		*header = cur; // 新链表的头指针指向待被转移的节点
	}
}

// 删除链表中所有值为val的节点
void del_node(node_t** header, int val)
{
	node_t* front = *header;
	node_t* back = NULL; // 记录被删除节点的前一个节点

	while(front)
	{
		if (front->data == val)
		{
			if (front == *header)
			{
				*header = front->next;
				front->next = NULL;
				free(front);
				front = *header;
				continue;
			}
			
			back->next = front->next;
			front->next = NULL;
			free(front);
			front = back->next;
			continue;
		}

		back = front;
		front = front->next;
	}
}

// 打印链表的内容
void print_link(node_t *header)
{
	node_t* tmp = header;

	if (NULL == header)
	{
		printf("empty link...\n");
		return;
	}

    do
    {
		printf("%d\n",tmp->data);	
    }while(tmp = tmp->next);
}

以上只是完成了链表的实现,到底应该怎么调用,请继续往下看

#include "link.h"

// test_case1函数测试的功能:
// 简单的往链表中头插10个节点
// 打印链表节点
// 删除链表
int test_case1();

// test_case2函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点,注意打印出的节点的值和test_case1中的order的不同
// 删除链表
int test_case2();

// test_case3函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点
// 翻转链表
// 打印链表节点,注意比较翻转前后链表节点的次序
// 删除链表
int test_case3();

// test_case4函数测试的功能:
// 简单的往链表中尾插10个节点
// 打印链表节点
// 删除值为8的节点
// 打印链表节点,注意比较删除前后链表值的变化
// 删除链表
int test_case4();

// test_case4函数测试的功能:
// 简单的往链表中尾插9个节点
// 打印链表节点
// 查找链表的中间节点
// 删除链表
int test_case5();

int test_case1()
{
	node_t* header = NULL;
	int i = 0;

    // 向链表中动态插入10个节点
	for (i = 0; i < 10; i++)
		head_append(&header, i);

	print_link(header);

	del_link(&header);
	print_link(header);
    return 0;
}

int test_case2()
{
	node_t* header = NULL;
	int i = 0;

    // 向链表中动态插入10个节点
	for (i = 0; i < 10; i++)
		tail_append(&header, i);

	print_link(header);

	del_link(&header);
	print_link(header);
    return 0;
}

int test_case3()
{
	node_t* header = NULL;
	int i = 0;

    // 向链表中动态插入10个节点
	for (i = 0; i < 10; i++)
		tail_append(&header, i);
    printf("翻转前的链表:\n");
	print_link(header);

    
	turnover_link(&header);
    printf("翻转后的链表:\n");
	print_link(header);

	del_link(&header);
	print_link(header);
    return 0;
}

int test_case4()
{
	node_t* header = NULL;
	int i = 0;

    // 向链表中动态插入10个节点
	for (i = 0; i < 10; i++)
		tail_append(&header, i);

    printf("节点删除前的链表:\n");
	print_link(header);

    // 删除一个val为8节点
    printf("节点删除后的链表:\n");
	del_node(&header, 8);
	print_link(header);

	del_link(&header);
	print_link(header);
    return 0;
}

int test_case5()
{
	node_t* header = NULL;
	node_t* mid = NULL;
	int i = 0;

    // 向链表中动态插入9个节点
	for (i = 0; i < 9; i++)
		tail_append(&header, i);
    printf("链表节点信息:\n");
	print_link(header);

	mid = find_mid(&header);
	printf("mid node data:%d\n", mid->data);

	del_link(&header);
	print_link(header);
    return 0;
}


int main(int argc, char** argv)
{
    int n = atoi(argv[1]);

    switch (n)
    {
    case 1:
        test_case1();
        break;
    case 2:
        test_case2();
        break;
    case 3:
        test_case3();
        break;
    case 4:
        test_case4();
        break;
    case 5:
        test_case5();
        break;
    default:
        printf("not support operate!\n");
    }

	return 0;
}

下面就是每个test_case执行的结果。

$ ./slink1 1
9
8
7
6
5
4
3
2
1
0
empty link...

$ ./slink1 2
0
1
2
3
4
5
6
7
8
9
empty link...

$ ./slink1 3
翻转前的链表:
0
1
2
3
4
5
6
7
8
9
翻转后的链表:
9
8
7
6
5
4
3
2
1
0
empty link...

节点删除前的链表:
0
1
2
3
4
5
6
7
8
9
节点删除后的链表:
0
1
2
3
4
5
6
7
9
empty link...

$ ./slink1 5
链表节点信息:
0
1
2
3
4
5
6
7
8
mid node data:4
empty link...

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sif_666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值