目录
1 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
物理结构:(数据实际上在内存中的内存位置,指针的值的变化)
逻辑结构:(为了便于理解,将指针形象化为箭头,指针值的变化反映为指针的移动)
2 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
2.戴头或者不戴头
3.循环或者非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了。
3 单向链表的实现
要实现的接口:
// 动态申请一个节点 SListNode* BuySListNode(SLTDataType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDataType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDataType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDataType x); // 单链表在pos位置之后插入x void SListInsertAfter(SListNode* pos, SLTDataType x); // 单链表删除pos位置之后的值 void SListEraseAfter(SListNode* pos); // 在pos的前面插入 void SLTInsert(SListNode** pphead, SListNode* pos, SLTDataType x); // 删除pos位置 void SLTErase(SListNode** pphead, SListNode* pos); // 删除链表 void SLTDestroy(SListNode** pphead);
一、定义链表的结点
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
typedef int SLTDataType; 作用:如果想要链表存储的数据类型是 float ,只需要修改这条语句的 int 就行了。另外,有人提出让链表的头结点的 data 存储链表的结点个数,根据链表结点的定义,这样做是错误的,因为 data 是 SLTDataType 类型,若该类型是 char ,当链表结点的个数超过128 个,就会出现问题。
不能写成:
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
SLTNode* next;
}SLTNode;
typedef 在定义完结构体后才起作用。
在继续展示下面的函数前,有必要弄清什么时候该使用 assert 断言指针为 NULL,什么时候不该使用。
该用的时候:实参指针为空是错误的时候。也就是说,实参指针永远不应该为空,为空是错误情况,这个时候就应该使用 assert 断言。
不该使用的情况:实参指针可以为空的时候。也就是说,实参指针为空代表一定的意义的时候,这个时候不应该使用 assert 断言。
二、链表的打印
void SLTPrint (SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
易错点
1、phead 是指向链表第一个结点的指针,可以为 NULL(表示链表没有数据),所以该函数不需要断言 phead 指针为空。但是对于顺序表,顺序表没有数据的标志是 size = 0,而不是 ps = NULL,所以打印顺序表的数据需要断言 ps
2、cur = cur->next;不能写成 cur++;除非 cur 是指向一个结构体数组,链表的结点是非连续的。
3、while(cur != NULL) 不能写成 while(cur->next != NULL) ,会导致最后一个数据没有打印。(while(cur != NULL) 也可以写成 while(cur))
三、链表的尾插
void SLPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
if(*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
易错点
1、不能断言 phead,phead == NULL 表示链表无数据,是合法的。
2、找尾部分不能写成:(这样链表链接不上)
// 找尾
SLTNode* tail = *pphead;
while (tail != NULL)
{
tail = tail->next;
}
tail = newnode;
3、if(*pphead == NULL)的判断十分有必要,不能省略。
4、要改变 phead 的内容,函数的参数应该是二级指针。如果不想使用二级指针,可以将函数的返回值的类型设计为 SLTNode* ,返回新的头指针。
5、由于 pphead 是指向 phead 的,pphead 应该永远不为 NULL,但为了避免疏忽大意带来的调试麻烦,有 pphead 为实参的函数都应该断言 pphead。
接下来要多次用到创建结点的操作,将以上创建结点的代码封装成一个函数:
SLTNode* BuyNode(SLTNode** pphead,int x)
{
assert(pphead);
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
return;
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
四、链表的头插
void SLTPushFront(SLTNode ** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
以上代码也能应对链表为空的情况
逻辑结构:
对比尾插会发现链表头插的效率更高。
五、链表的尾删
void SLTPopBack (SLTNode** pphead)
{
assert(pphead);
//链表为空
//温柔的检查
//if(*PPhead == NULL)
//{
// return;
//}
//暴力的检查
assert(*pphead != NULL);
if(*pphead->next == NULL)
{
free(*PPhead);
*pphead = NULL;
return;
}
// 找尾
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
注意点:
1、在找尾的过程中,tail 在指向下一个结点之前,把 tail 赋值给 prev ,便于在找到最后的结点时将最后的结点的上一个结点的 next 指针置为空。
2、结点的存储空间在堆区,删除结点后要释放空间。
3、要考虑到只有一个结点和链表为空的情况,用 if 语句判断一下。
另一种更巧妙的办法:
void SLTPopBack (SLTNode** pphead)
{
assert(pphead);
//链表为空
//温柔的检查
//if(*PPhead == NULL)
//{
// return;
//}
//暴力的检查
assert(*pphead != NULL);
if(*pphead->next == NULL)
{
free(*PPhead);
*pphead = NULL;
return;
}
// 找尾
SLTNode* tail = *pphead;
while (tail->next->next != NULL)
{
tail = tail->next;
}
free(tail->next);
prev->next = NULL;
}
六、链表的头删
void SLTPopFront(SLTNode ** pphead)
{
assert(pphead);
// 暴力检查
assert(*pphead);
// 温柔的检查
//if (*pphead == NULL)
//return;
SLTNode* first = *pphead;
*pphead = first->next;
free(first);
first = NULL;
}
对比尾删会发现链表头删的效率更高。
七、链表的查找
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
注意:空链表也能查找,只是返回空指针。
八、在 pos 位置的前面插入
void SLTInsert(SLTNode ** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == * pphead)//在第一个位置插入,不就是头插吗
{
SLTPushFront(pphead, x);
}
else
{
// 找到pos的前一个位置
SLTNode* prev = * pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
逻辑结构:
注意:
1、pos 位置是否在链表上是函数使用者考虑的事,上面的函数不保证 pos 位置是否在链表上,但应该保证 pos 至少不是 NULL。
2、在 pos 位置的后面插入的复杂性比在前面插入的复杂性小,因此在 pos 位置的前面插入的问题可以转化为在 pos 后面插入然后交换 pos 和插入的数据的位置。
九、在 pos 位置的后面插入
avoid SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
pos 指针指向链表的一个结点,该结点的地址由链表的查找函数(上面的第八个函数)返回。
以上代码不能写成:
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
pos->next = newnode;
newnode->next = pos->next;
}
这会导致 nownode 的 next 成员指向的是它自己。逻辑结构:
十、在 pos 位置删除
void SLTErase(SLTNode ** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
//assert(*pphead);
if (*pphead == pos)
{
SLTPopFront(pphead);//就是前删
}
else
{
// 找到pos的前一个位置
SLTNode* prev = * pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
注意:在调用完该函数后,一般由该函数的调用者来 free(pos)。
十一、在 pos 后面删除
void SLTEraseAfter (SLTNode* pos)
{
assert(pos);
assert(pos->next);
SLTNode* del = pos->next;//保存pos位置下一个结点的地址
pos->next = pos->next->next;
free(del);
del = NULL;
}
以上代码不能写成:
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);
pos->next = pos->next->next;
}
这会导致 pos 后面的结点的地址找不到。
十二、链表的销毁
void SLTDestroy (SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
SLTNode* tmp = cur->next;
free(cur);
cur = tmp;
}
}
4 双向循环链表的实现
双向循环链表的初始形态应该是怎样呢?也就是说,如果一个双向循环链表没有数据,它该是怎样呢?对于顺序表来说,没有数据的标志是 size == 0,对于单向非循环链表来说,没有数据的标志是 phead == NULL,那对于双向循环链表来说,应该是怎样呢?
会是以下的样子吗?
如果真是这样,就没有体现循环的特点了,所以应该是这样:
一、定义双向循环链表的结点
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
二、双向循环链表的初始化
void LTInit(LTNode** pphead)
{
*pphead = BuyListNode(-1);
*phead->next = phead;
*phead->prev = phead;
}
如果不想使用二级指针,可以将函数的返回值类型设计为 LTNode* 类型,返回头指针 phead。
LTNode* LTInit ()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
三、 双向循环链表的打印
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* cur = phead->next;
printf("<=head=>");
while (cur != phead)
{
printf("%d<=>", cur->data);
cur = cur->next;
}
printf("\n");
}
注意:cur 指针开始时指向 head 的下一个结点,当 cur 指向 head 时停止打印。
四、 双向循环链表的尾插
void LTPushBack (LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
1、相比于单向链表的尾插,双向循环链表的尾插不需要判断链表是否为空,不需要找尾,不需要使用二级指针(函数中改变的都是结构体的成员)十分方便。
2、要断言 phead,因为 phead 永远不能为空,phead 为空是错误情况。
上面使用的 BuyListNode 函数:
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = ( LTNode*)malloc (sizeof (LTNode) );
if (node == NULL)
{
perror("malloc fail");
return NULL;
}
node->next = NULL;
node->prev = NULL;
node->data = x;
return node;
}
在展示双向循环链表的尾删的函数之前,先展示判断双向循环链表是否为空的函数:
五、判断双向循环链表是否为空
bool is_LTEmpty (LTNode* phead)
{
assert(phead);
/*if (phead->next == phead)
{
return true;
}
else
{
return false;
}*/
return phead->next == phead;
该函数可以直接返回 phead->next == phead 。
六、双向循环链表的尾删
void LTPopBack (LTNode* phead)
{
assert(phead);
assert(!is_LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
tail = NULL;
}
注意:assert( ! is_LTEmpty(phead)); !不要忘记写,assert ()括号内表达式为假才会报错。
七、双向循环链表的头插
void LTPushFront (LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
}
注意:不能一开始就 phead->next = newnode,因为这样原先的第一个数据的地址就不好找到,正确的做法是先将 newnode->next = phead->next;
八、在 pos 位置之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
有了上面的函数,双向循环链表的头插函数将大大简化:
void LTPushFront (LTNode* phead, LTDataType x)
{
assert(phead);
LTInsert(phead->next,x);
}
尾插函数也是,但尾插函数的 pos 是 phead :
void LTPushBack (LTNode* phead, LTDataType x)
{
assert(phead);
LTInsert(phead,x);
}