什么叫结构体?就是能够将不同数据类型集合在一起构造一个新的数据类型的东西,它有一个注意点就是不能引用自身作为结构体成员,为什么呢?因为在创建这种类型的结构体变量时计算机无法得知给结构体变量分配多大的内存导致编译器报错,提示非法操作。那么为什么计算机无法给结构体变量分配某个固定内存呢?是这样的,如果你的结构体原先已经存在两个int型变量了,如果计算机是四字节对齐的话那么结构体类型目前已经占据了8个字节了,然后又在int型变量后面将结构体本身作为成员使用,那么你认为计算机是分配多少字节?是16个(原先的两个int加上后来的两个int)?还是说结构体里套结构体就这样一直套下去?显然两种都是不正确的,因为计算机无法继续进行下去了所以只能报错提示操作不合法了。但是,结构体虽然不能引用自身作为成员变量,而它却可以引用自身结构体指针变量作为新成员。为什么这就可以了呢?因为不管哪种指针变量占用的内存大小都是4个字节所以计算机能够正确的分配内存,这样就引出了今天的主题----链表。
什么叫链表?它由许多节点(包括头结点和数据节点)构成。每个节点的特点就是它本身是一个结构体变量,但是他的成员包括数据域和指针域,数据域存放数据内容,指针域就存放指向其他节点。链表有一个好处就是事先无需知道数据量,它可以实时动态分配节点内存空间并高效的在任意节点处插入新节点或删除节点。链表分为带头结点的和不带有节点的,单向的,双向的,循环的。下面看看一个简单是链表是如何用图示表示出来的。
typedef void LinkList;
//节点指针域定义
typedef struct _tag_LinkListNode
{
struct _tag_LinkListNode* next;
}LinkListNode;
typedef struct _tag_LinkList{
LinkListNode header;
int length;
}TLinkList;
typedef struct Value
{
LinkListNode header;
int dat;
}Val;
这就是定义一个节点的内容。
这里值得说一下的是在节点中将指针域单独拿出来定义,并作为节点的第一个成员,这样结构体变量的地址就和指针域的地址相同,指向结构体就相当于直接指向了该节点。这样做的好处是加强了通用性,链表就可以连接不同数据类型的节点,是不是对数据的封装进一步感觉到惊叹。
链表就是由一些包含数据域和指针域的节点构成的,当前节点的指针域指向下一个节点,最后一个节点的指针域就指向NULL,就是这样不断连接构建了长度随时可变的链表,相比于线性表它有更大的灵活性,不会浪费任何空间,并且链表头节点的数据域还有个特殊的功能,就是记录链表的长度。
链表的功能和操作也就是一些增、删、改、查等等。下面开始正式介绍链表的实现。
1、创建
创建链表即创建一个头结点,将length置0,header的next成员指向NULL。一般尽量在堆区上创建,因为在一个程序运行时分配的空间中堆区空间十分大,相比于栈的要大得多,并且栈在一个函数结束后就会释放函数运行时的活动记录,而堆虽然有内存泄漏的风险,但是一般记住free还是不会发生比较严重的错误。
代码如下:
LinkList* LinkList_Create()
{
TLinkList *ret = (TLinkList *)malloc(sizeof(TLinkList));
if(ret != NULL)
{
ret->length = 0;
ret->header.next = NULL;
}
return ret;
}
函数流程也正如我上面所说,就不再解释。
2、插入
在前面线性表中说到过线性表的插入是比较费劲的,那么这里的插入就将是无比轻松加愉快的,它不需要移动任何节点,简单的两个步骤就能搞定,不多说,上图看一下:
这里只需要两步就能将一个新节点插入到链表当中。1、将新节点的指针域指向当前节点的指针域2、将当前节点指向新节点。此处注意两步顺序不能交换,否则就会中断链表。原理是不是很简单,但是一些小细节还是需要注意的,程序哪怕出了一丁点错就会提出抗议。下面看看执行代码;
int LinkList_Insert(LinkList* list, LinkListNode* node, int pos)
{
TLinkList* sList = (TLinkList *)list;
LinkListNode* Node = (LinkListNode*)malloc(sizeof(LinkListNode));
Node = node;
int ret = (sList != NULL) && (pos >= 0) && (Node != NULL);
int i = 0;
if(ret)
{
LinkListNode* current = (LinkListNode *)sList;
for(i = 0; (i < pos ) && (current->next != NULL); i++)//移动指针
{
current = current->next;
}
//新节点和原先的节点进行连接
Node->next = current->next;
current->next = Node;
sList->length++;
}
return ret;
}
程序的核心也就是那两步,首先malloc一个节点大小的空间,检验一些参数的合法性,但是在插入之前需要遍历一下链表,找到要插入的位置pos,最后将length自增,便于获取链表的实际长度。
获取节点的操作总共分为两步,首先定位到要获取节点的上一个位置,再就是直接返回当前节点的指针域即可,即获取的是一个指针,这里需要注意下,调试打印时需要进行一些强制类型转换。
3、删除
删除的原理相比插入更加简单,先上图直观的看一下再说。
就一步操作,将要删除的节点的前一个节点的指针域指向它的下一个节点的指针域,但是有一点需要注意,删除后必须将节点释放掉,不然可能造成内存泄漏。
LinkListNode* LinkList_Delete(LinkList* list, int pos)
{
TLinkList* sList = (TLinkList*)list;
LinkListNode* ret = NULL;
int i = 0;
if( (sList != NULL) && (0 <= pos) && (pos < sList->length) )
{
LinkListNode* current = (LinkListNode*)sList;
for(i=0; i<pos; i++)//节点移动
{
current = current->next;
}
ret = current->next;//节点定位到pos
free(current->next);
current->next = ret->next;//删除操作
sList->length--;
}
return ret;
}
看起来是不是很简单呢。首先判断删除的节点是否合法,不合法就没有执行下去的意义了,只有直接退出了,然后是定位节点,删除节点。
4、逆置
最后一个大问题,链表的逆置,我觉得这个问题还是有一定难度的,我试着解释清楚。
逆置就是将链表数据节点的顺序颠倒一下,而且是一个节点一个节点的逆置,首先我们知道,空表不需要逆置,一个节点不需要逆置,只有当节点数大于等于二才有意义。
具体图如下;
所以我们就需要一个逆置的起始状态,若PHead表示头结点,PPre表示前一个节点,PCur表示当前节点。那么,
PPre = PHead->next;
PCur = PHead->next->next;
就是从这个初始状态开始逆置,具体逆置原理就是不断的让PCur指向PPre,然后让PCur和PPre往右移动一个位置 ,直到PCur为NULL代表交换结束。
具体是实现代码如下:
int LinkList_Reverse(LinkList *list)
{
LinkListNode *pPre = NULL,*pCur = NULL,*tmp = NULL;
LinkListNode *pHead = (LinkListNode*)list;
//是否需要逆置检测,空表不需要逆置,一个元素的不需要逆置,至少要有两个节点才能完成逆置
if(pHead == NULL || pHead->next == NULL || (pHead->next)->next == NULL)
{
return 0;
}
//起始状态
pPre = pHead->next;//第一个节点
pCur = pHead->next->next;//第二个节点
//开始逆置
while(pCur)
{
tmp = pCur->next;//缓存后面一个节点
pCur->next = pPre;//逆置
pPre = pCur;//pPre下移一个节点
pCur = tmp;//指向下一个节点
}
//头节点指向尾部节点
(pHead->next)->next = NULL;//第一个节点的指针域指向NULL
pHead->next = pPre;//当while结束过后pPre指向的是最后一个节点,然后将头节点指向pPre
return 0;
}
根据程序的注释以及结合图形看差不多应该能理解过程了,最重要的就是逆置的模型需要理解。
直到现在,链表的基本操作也就结束了,实际上逆置的实现可以用双向链表更好的实现,我们需要学习这个过程来加深链表的成型和使用。