链表

上篇文章用C语言简单实现了一个可变数组,但是,它有一个缺点:每一次变大时,都要申请一块新的内存空间,可以容纳下全部数据,然后再进行原来空间向新空间数据的拷贝。随着数组增大,数据越来越多,拷贝需要花很多时间。所以这种方法不高效,试想我们可以采用链表的方式:原来的内存不动,如果不够用,这时不是再申请更大一块再进行数据拷贝,而是就申请一个BLOCK大的一块内存,然后把它们链起来,如下:

这样不仅避免了反复的拷贝,而且可以利用内存的每一个角落十分高效。

结构示意图如下:

程序背景:不断读入number,直到读到-1为止,则程序结束。


方便起见,这里逐一对每一个函数进行分开讲解。首先是:添加元素。

这里我们首先可以直接在main函数中按照逻辑写下代码,然后再放入函数中。

list.h文件

#ifndef _NODE_H_
#define _NODE_H_

typedef struct _node
{
	int value;
	struct _node*next;
}Node;


#endif

list.c文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
#include<stdio.h>
#include<stdlib.h>


int main()
{
	Node*head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			//add to linked-list
			Node*p = (Node*)malloc(sizeof(Node));
			p->value = number;
			p->next = NULL;
			//find the last 遍历,找到最后一个节点,接上去
			Node*last = head;
			while (last->next)
			{
			   last = last->next;
			}
			//attach
			last->next = p;
		}
	} while (number != -1);

	system("pause");
	return 0;
}

但是这个代码是有问题的。开始时,head指向NULL,赋给last指针,这时last也指向空,下面while语句判断last->next明显会出错,空指针的arrow是无效的。所以我们要用last,必须判断last是否为空。所以代码改为-->

int main()
{
	Node*head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			//add to linked-list
			Node*p = (Node*)malloc(sizeof(Node));
			p->value = number;
			p->next = NULL;
			//find the last
			Node*last = head;
			if (last)//判断last指针是否为空,不为空才可以进行next的访问
			{
				while (last->next)
				{
					last = last->next;
				}
				//attach
				last->next = p;
			}
			else
			{
				head = p;
			}
		}
	} while (number != -1);


   //打印所有数字
   Node*p;
   p = head;
   while (p)
   {
       printf("%d ", p->value);
       p = p->next;
   }
	system("pause");
	return 0;
}

输入:1 2 3 -1,运行结果:

综上,我们所做的事是:得到一个number后,制造了一个节点,并把它链在链表尾部。如果设计成函数,那么应该怎么写?把上面在main函数中的代码截取下来,封装在add函数中。得到如下

void add(Node*head, int number);
int main()
{
	Node*head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			add(head, number);
		}
	} while (number != -1);

		//打印所有数字
		Node*p;
		p = head;
		while (p)
		{
			printf("%d ", p->value);
			p = p->next;
		}
	system("pause");
	return 0;
}

void add(Node*head, int number)
{
	//add to linked-list
	Node*p = (Node*)malloc(sizeof(Node));
	p->value = number;
	p->next = NULL;
	//find the last
	Node*last = head;
	if (last)
	{
		while (last->next)
		{
			last = last->next;
		}
		//attach
		last->next = p;
	}
	else
	{
		head = p;
	}
}

输入:1 2 3 -1,输出结果,这里并没有打印出1,2,3。说明程序出了问题。函数中的参数传入head,并且在函数中修改了head,但是main函数中的Node*head = NULL;,这个head并没有被修改,所以每次出了add的函数作用域,head仍然为NULL,这就导致每次加入新节点,head都是指向新节点的,所以一连串的节点并没有被链起来,这就导致我们遍历时,程序没有输出每个节点的value值。图解如下-->

这里有几种方案可以解决这个问题,下面分别对这几种方案做个归纳,对比。

方案一:add函数返回一个Node*指针,函数结束时,把head传出去,在调用函数的地方head=add(head,number)接收。

//main函数中调用add函数的部分用head来接收返回值
head=add(head, number);

//add函数返回值不再是void,而是返回Node*的指针
Node* add(Node*head, int number)
{
	//add to linked-list
	Node*p = (Node*)malloc(sizeof(Node));
	p->value = number;
	p->next = NULL;
	//find the last
	Node*last = head;
	if (last)
	{
		while (last->next)
		{
			last = last->next;
		}
		//attach
		last->next = p;
	}
	else
	{
		head = p;
	}
	return head;
}

输入:1 2 3 -1,输出结果,程序可以正常遍历链表,打印出每个节点的值。但是这样做还是有个小缺点:需要使用add函数的程序员必须记得在调用add函数时,用head把返回值接收过来;如果忘了,那么对空链表的add就是错的。

方案二:add函数中不传head,而是传入head的指针(即head=add(&head, number);)那么add函数就要改为Node* add(Node**head, int number),这样的话最后要不要有返回值就无所谓了,因为传入的是指向指针的指针,这就使我们能够在函数内部对指针的值做修改。可以对比swap交换两个数的场景。

//add函数声明
void add(Node**phead, int number);
int main()
{
	Node*head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			add(&head, number);//传入head指针的地址
		}
	} while (number != -1);

		//打印所有数字
		Node*p;
		p = head;
		while (p)
		{
			printf("%d ", p->value);
			p = p->next;
		}
	
	system("pause");
	return 0;
}
//传入指向head的指针
void add(Node**phead, int number)
{
	//add to linked-list
	Node*p = (Node*)malloc(sizeof(Node));
	p->value = number;
	p->next = NULL;
	//find the last
	Node*last = *phead;
	if (last)
	{
		while (last->next)
		{
			last = last->next;
		}
		//attach
		last->next = p;
	}
	else
	{
		*phead = p;
	}
}

输入:1 2 3 -1,输出结果,程序可以正常遍历链表,打印出每个节点的值。

方案三:再定义一个结构体List,成员是Node*head,,add函数传入List*的一个指针。

//再定义一个结构体
typedef struct _list
{
	Node*head;
}List;

//函数声明
void add(List*plist, int number);
int main()
{
	List list;
	list.head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			add(&list, number);
		}
	} while (number != -1);

		//打印所有数字
		Node*p;
		p = list.head;
		while (p)
		{
			printf("%d ", p->value);
			p = p->next;
		}
	
	system("pause");
	return 0;
}

void add(List*plist, int number)
{
	//add to linked-list
	Node*p = (Node*)malloc(sizeof(Node));
	p->value = number;
	p->next = NULL;
	//find the last
	Node*last = plist->head;
	if (last)
	{
		while (last->next)
		{
			last = last->next;
		}
		//attach
		last->next = p;
	}
	else
	{
		plist->head = p;
	}
}

输入:1 2 3 -1,输出结果

表面上看起来这种结构(传list指针)与上一种方法(传head)指针是一回事,本质都一样,但是这种方法的好处在于:我们定义了一种自己定义的数据结构List来代表整个链表,现在在这个List结构中只放了一个head,但是以后可以有各种扩充:比如上述代码中,每次链新节点的时候,都要用last指针遍历一遍前面整个链表,找到当前的最后一个节点位置,链上去,如果可以有一个tail指针,记录当前最后一个节点位置,那么就不用每次加入新节点的时候遍历链表找最后一个节点位置了,直接链在tail指针所指的节点后就可以。结构设计为-->

typedef struct _list
{
    Node*head;
    Node*tail;
}List;

总结起来,这种设计结构的好处就是:便于将来改进List。如果不这样设计,就只是一个悬在外面的head。对于一个工程化的程序来说,合理安排结构,是很有必要的,不单单只是学会基本的“增删查改”。所以这种思想也值得我们学习。


打印函数:

void print(List *plist);
int main()
{
	List list;
	list.head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
            //增加新元素
			add(&list, number);
		}
	} while (number != -1);

	//调用打印函数
	print(&list);
	system("pause");
	return 0;
}
//打印函数->第一种写法用while循环
void print(List *plist)
{
	Node*p;
	p = plist->head;
	while (p)
	{
		printf("%d ", p->value);
		p = p->next;
	}
}
//打印函数->第二种写法用for循环
void print(List *plist)
{
	Node*p;
	for (p = plist->head; p; p=p->next)
	{
		printf("%d ", p->value);
	}
	printf("\n");
}



查找函数:

void search(List *plist, int number)
{
	int isfound = 0;
	Node*p;
	p = plist->head;
	while (p)
	{
		if (p->value == number)
		{
			printf("找到了\n");
			isfound = 1;
			break;
		}
		p = p->next;
	}
	if (!isfound)
	{
		printf("没找到\n");
	}
}

删除:

删除节点的时候,比如删除p节点。第一件事:首先要找到p前面的那个节点,改变它的指针指向,让它指向p后面的那个节点;第二件事:free(p)。问题就在于P前面的那个节点是什么?如何找到?

由于是单向链表。只有指向下一个节点的指针,并没有指向前一个节点的指针,所以得循环遍历找到前一个节点。若有另一个指针q记录前一个节点,则只需要改变q->next的指针指向,让它指向p->next;然后free(p)就可以了。图示如下:

代码:

//删除一个节点
void remove(List *plist, int number)
{
	Node*p;
	Node*q;
	for (q = NULL, p = plist->head; p; q = p, p = p->next)
	{
		if (p->value == number)
		{
			if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
			{
				q->next = p->next;
			}
			else
			{
				plist->head = p->next;
			}
			free(p);
			break;
		}
	}
}

注:这里删除节点的函数只考虑了number与value相等的情况(即对应节点存在),还会有删除一个不存在的节点情况,完整的函数设计应该先调用search函数,利用search函数的返回值判断对应节点是否存在,如果存在再进行删除。由于本节只是对链表的一个初探索,主要熟悉结构的定义与设计,所以这里就没有考虑那么完善;包括search函数,也是没有设计返回值的。


清除整个链表

void _delete(List *plist)
{
	Node*p;
	Node*q;
	for (p = plist->head; p; p = q)
	{
		q = p->next;
		free(p);
	}
	plist->head = NULL;//全部节点删除后,一定记得把head节点置空
}

全部代码如下:

list.h文件

#ifndef _NODE_H_
#define _NODE_H_

typedef struct _node
{
	int value;
	struct _node*next;
}Node;


#endif

list.cpp文件

#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"
#include<stdio.h>
#include<stdlib.h>

//typedef struct _node
//{
//	int number;
//	struct _node*next;
//}Node;

typedef struct _list
{
	Node*head;
}List;


//Node* add(Node*head, int number);
//void add(Node**phead, int number);
void add(List*plist, int number);
//Node* add(Node*head, int number);
void print(List *plist);
void search(List *plist, int number);
void remove(List *plist, int number);
void _delete(List *plist);
int main()
{
	//Node*head = NULL;
	List list;
	list.head = NULL;
	int number;
	do
	{
		scanf("%d", &number);
		if (number != -1)
		{
			//add(&head, number);
			add(&list, number);
		}
	} while (number != -1);

		//打印所有数字
		/*Node*p;
		p = list.head;
		while (p)
		{
			printf("%d ", p->value);
			p = p->next;
		}*/

	print(&list);
	//scanf("%d", &number);
	//search(&list, number);


	//Node*p;
	//Node*q;
	//for (q = NULL, p = list.head; p; q = p, p = p->next)
	//{
	//	if (p->value == number)
	//	{
	//		if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
	//		{
	//			q->next = p->next;
	//		}
	//		else
	//		{
	//			list.head = p->next;
	//		}
	//		free(p);
	//		break;
	//	}
	//}

	
	//remove(&list, number);
	//print(&list);

	//删除整个链表
		/*for (p = head; p; p = q)
		{
			q = p->next;
			free(p);
		}*/
	_delete(&list);
	print(&list);
	system("pause");
	return 0;
}


void add(List*plist, int number)
//void add(Node**phead, int number)
{
	//add to linked-list
	Node*p = (Node*)malloc(sizeof(Node));
	p->value = number;
	p->next = NULL;
	//find the last
	Node*last = plist->head;
	if (last)
	{
		while (last->next)
		{
			last = last->next;
		}
		//attach
		last->next = p;
	}
	else
	{
		//*phead = p;
		plist->head = p;
		//head = p;
	}
}

//void print(List *plist)
//{
//	Node*p;
//	p = plist->head;
//	while (p)
//	{
//		printf("%d ", p->value);
//		p = p->next;
//	}
//}

void print(List *plist)
{
	Node*p;
	for (p = plist->head; p; p=p->next)
	{
		printf("%d ", p->value);
	}
	printf("\n");
}

void search(List *plist, int number)
{
	int isfound = 0;
	Node*p;
	p = plist->head;
	while (p)
	{
		if (p->value == number)
		{
			printf("找到了\n");
			isfound = 1;
			break;
		}
		p = p->next;
	}
	if (!isfound)
	{
		printf("没找到\n");
	}
}

void remove(List *plist, int number)
{
	Node*p;
	Node*q;
	for (q = NULL, p = plist->head; p; q = p, p = p->next)
	{
		if (p->value == number)
		{
			if (q)//这里一定要主要判断q,如果删的是第一个节点,则循环一进来q就是NULL,不能进行q->next;而应该让list.head = p->next;
			{
				q->next = p->next;
			}
			else
			{
				plist->head = p->next;
			}
			free(p);
			break;
		}
	}
}

void _delete(List *plist)
{
	Node*p;
	Node*q;
	for (p = plist->head; p; p = q)
	{
		q = p->next;
		free(p);
	}
	plist->head = NULL;//全部节点删除后,一定记得把head节点置空
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值