C语言 数据结构笔记1 | 单链表

一、链表的由来

数组是最简单的数据结构,链表复杂一些,二叉树、图则更加复杂的数据结构。数据结构由简单到复杂,它们所要解决的问题也是由简单到复杂。要学习复杂的数据结构就要先学习简单的数据结构,如果简单的数据结构可以解决问题,就没必要使用复杂的数据结构。数组天生的缺陷导致的它解决不了某些问题,所以人们发明了链表。
数组的三个特点:
一:数组中所有的元素类型必须相同;
二:数组在定义的时候需要明确指定数组元素的个数,并且一般来说个数是不能改变的(Linux内核中会使用变长数组,在高级语言如C++当中也支持变成数组);
三:某个元素的移动可能造成元素的大面积移动,效率不高。这些特点,也使得数组有了简单易用的好处,但是同样也带来了缺陷。那如何弥补这些缺陷?
一:数组的第一个缺陷靠结构体解决。结构体允许其中的元素类型不同。
二:数组的第二个缺陷有两个解决思路:1、使用变长的数组。2、使用链表。
三:针对第三个缺陷使用链表是最好的解决方法。

二、单链表实现

单链表的创建到使用大致有如下几步:
1、创建空的单链表,比如可以定义一个CreatNode()函数来创建第一个节点。
2、操作单链表(增、删、查、更改、排序),比如说插入操作,定义一个InsertTail()函数来向单链表(或是节点)的后面追加新节点;
3、销毁单链表,比如定义一个DestroyList()函数,用于销毁链表。

1、构建第一个节点

单链表的节点构成

在C语言中节点的创建方法就是定义一个结构体。

struct node 
{
     
	int data;
	struct node* pNext;
};

  • 注意:这里只是定义了一个结构体类型,本身并没有变量的生成,故不占用内存(比如说数据类型 int、char 、float,它们是不占用内存空间)。

使用堆内存创建一个节点

为什么要使用对内存?用来形成链表的内存一般有如下特点:必须需要多少有多少;必须可以随意删除和释放。根据上面两个特点,我们就知道堆内存是最适合用来做链表的节点的。
下面是创建节点函数的伪代码:

CreatNode(节点中要存的数据)
{
     
	申请一个节点大小的堆内存;
	检查堆内存是否申请成功;
	清理申请到的堆内存;
	填充节点的数据;
	节点中指针初始化为NULL}

下面是伪代码的实现过程:

//创建一个新节点
typedef struct node StrNode_T;
StrNode_T* CreatNode(int data)
{
     
	StrNode_T* p = (StrNode_T*)malloc(sizeof(StrNode_T));
	if (NULL == p)
	{
     
		printf("malloc 申请失败!!!!");
		return;
	}
	memset(p, 0 ,sizeof(StrNode_T));
	p->data = data;
	p->pNext = NULL;
	return p;
}

函数的返回值,是一个指向刚刚创建出来的节点的首地址的指针。

链表的头指针

头指针并不是节点,而是一个普通指针变量,占4个字节。头指针类型是struct node*类型,所以它才能指向链表的节点。
一个典型的链表实现是:头指针指向链表的第一个节点,然后第一个节点中的指针指向下一个节点,然后依次类推,这样就构成了一个链表。

构建第一个简单的链表:
1、定义头指针。
2、创建第一个节点,并将头指针指向第一个节点。
3、接着创建节点,并将新的节点从前一个节点的尾部插入进来。
4、以此类推,需要存多少数据便创建多少个数据,最终形成链表。

int main(void)
{
     
	StrNode_T* pHeadNode = NULL;
	pHeadNode = CreatNode(0);
	return 0;
}

2、单链表实现尾部插入节点

主要要两步:
1、找到链表的最后一个节点
2、将新的节点和原来的最后一个节点连接

插入函数

//从尾部插入一个节点
void InsertTail(StrNode_T* pHeadNode, StrNode_T* pNewNode)
{
     
	if (NULL == pHeadNode)
	{
     
		return;
	}
	//找到链表的尾节点
	StrNode_T* p = pHeadNode;
	while (NULL != p->pNext)
	{
     
		p = p->pNext;
	}
	p->pNext = pNewNode;
}

打印函数

//打印函数
void print(StrNode_T* pHeadNode)
{
     
	int i = 0;
	StrNode_T* pTemp = pHeadNode;
	if (NULL == pHeadNode)
	{
     
		return;
	}
	while (pTemp != NULL)
	{
      
		printf("node%d = %d\n",i,pTemp->data);
		i++;
		pTemp = pTemp->pNext;
	}
}

main函数

int main(void)
{
     
	StrNode_T* pHeadNode = NULL;
	pHeadNode = CreatNode(0);
	InsertTail(pHeadNode, CreatNode(1));
	InsertTail(pHeadNode, CreatNode(2));
	InsertTail(pHeadNode, CreatNode(3));
	print(pHeadNode);

	return 0;
}

我们的程序到此为止,完成了两步,第一步、创建节点。第二步、插入节点。每次插入的时候都会用用创建函数创建节点,所以每次都会用malloc去申请,所以我们还要去对这些申请的内存去释放。第三步、释放链表。

Free函数

//释放空间
void Free(StrNode_T* pHeadNode)
{
     
	if (NULL == pHeadNode)
	{
     
		return;
	}
	StrNode_T* pFree = pHeadNode;
	while (pFree->pNext != NULL)
	{
     
		pHeadNode = pFree->pNext;
		free(pFree);
		pFree = pHeadNode;
	}
	free(pHeadNode);
}

所以main函数是:

int main(void)
{
     
	StrNode_T* pHeadNode = NULL;
	pHeadNode = CreatNode(0);
	InsertTail(pHeadNode, CreatNode(1));
	InsertTail(pHeadNode, CreatNode(2));
	InsertTail(pHeadNode, CreatNode(3));
	print(pHeadNode);
	Free(pHeadNode);
	return 0;
}

3、单链表实现头部插入节点

头部插入有两个步骤:
1、新节点的pNext指向原来的第一个节点的首地址,即新节点和原来的第一个节点相连。
2、头节点的pNext指向新节点的首地址,即头头节点和新节点相连。
伪代码的实现:

insert_head()
{
     
	第一步:新节点的pNext指向原来的第一个节点;
	第二步:头节点的pNext指向新节点;	
}

InsertHead

//头插法
void InsertHead(StrNode_T* pHeadNode, StrNode_T* pNewNode)
{
     
	pNewNode->pNext = pHeadNode->pNext;
	pHeadNode->pNext = pNewNode;

}

三、单链表的算法

1、遍历节点

其实前面已有实现了,比如打印链表的节点数据,和释放链表的时候就是通过遍历链表来进行,打印和释放的!
什么是遍历呢?
数据有存就肯定会取,既然链表是用来存放数据的,那么肯定要有从链表中读取数据的方法,这个方法就是遍历链表,也就是把链表中的各个节点挨个拿出来访问。
遍历的要求:不能遗漏、不能重复、效率要尽量的高。

2、如何遍历单链表

你可能感兴趣的:(C语言,数据结构,数据结构,c语言,单链表)