数组是最简单的数据结构,链表复杂一些,二叉树、图则更加复杂的数据结构。数据结构由简单到复杂,它们所要解决的问题也是由简单到复杂。要学习复杂的数据结构就要先学习简单的数据结构,如果简单的数据结构可以解决问题,就没必要使用复杂的数据结构。数组天生的缺陷导致的它解决不了某些问题,所以人们发明了链表。
数组的三个特点:
一:数组中所有的元素类型必须相同;
二:数组在定义的时候需要明确指定数组元素的个数,并且一般来说个数是不能改变的(Linux内核中会使用变长数组,在高级语言如C++当中也支持变成数组);
三:某个元素的移动可能造成元素的大面积移动,效率不高。这些特点,也使得数组有了简单易用的好处,但是同样也带来了缺陷。那如何弥补这些缺陷?
一:数组的第一个缺陷靠结构体解决。结构体允许其中的元素类型不同。
二:数组的第二个缺陷有两个解决思路:1、使用变长的数组。2、使用链表。
三:针对第三个缺陷使用链表是最好的解决方法。
单链表的创建到使用大致有如下几步:
1、创建空的单链表,比如可以定义一个CreatNode()函数来创建第一个节点。
2、操作单链表(增、删、查、更改、排序),比如说插入操作,定义一个InsertTail()函数来向单链表(或是节点)的后面追加新节点;
3、销毁单链表,比如定义一个DestroyList()函数,用于销毁链表。
在C语言中节点的创建方法就是定义一个结构体。
struct node
{
int data;
struct node* pNext;
};
为什么要使用对内存?用来形成链表的内存一般有如下特点:必须需要多少有多少;必须可以随意删除和释放。根据上面两个特点,我们就知道堆内存是最适合用来做链表的节点的。
下面是创建节点函数的伪代码:
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;
}
主要要两步:
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;
}
头部插入有两个步骤:
1、新节点的pNext指向原来的第一个节点的首地址,即新节点和原来的第一个节点相连。
2、头节点的pNext指向新节点的首地址,即头头节点和新节点相连。
伪代码的实现:
insert_head()
{
第一步:新节点的pNext指向原来的第一个节点;
第二步:头节点的pNext指向新节点;
}
InsertHead
//头插法
void InsertHead(StrNode_T* pHeadNode, StrNode_T* pNewNode)
{
pNewNode->pNext = pHeadNode->pNext;
pHeadNode->pNext = pNewNode;
}
其实前面已有实现了,比如打印链表的节点数据,和释放链表的时候就是通过遍历链表来进行,打印和释放的!
什么是遍历呢?
数据有存就肯定会取,既然链表是用来存放数据的,那么肯定要有从链表中读取数据的方法,这个方法就是遍历链表,也就是把链表中的各个节点挨个拿出来访问。
遍历的要求:不能遗漏、不能重复、效率要尽量的高。