数据结构单链表(不带头节点)实现源代码需要二级指针问题解释以及对比带头结点(不需要二级指针)
在数据结构链表部分,难免会因为c语言基础不够扎实而导致代码逻辑等出现问题,尤其是指针部分的应用,在不带头的单链表和带头的链表中指针的应用尤其显著,要充分理解指针部分的应用。在源代码后讲依靠源代码中指针的命名解释为什么在单链表增删查改中使用二级指针。
源代码
Slist.h头文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef int SLTDataType;
struct SListNode
{
SLTDataType data;
struct SListNode* next;
};
typedef struct SListNode SLTNode;
void SListPrint(SLTNode* phead);
void SListPushBack(SLTNode** pphead, SLTDataType x);
void SListPushFront(SLTNode* pphead, SLTDataType x);
void SListPopFront(SLTNode** pphead);
void SListPopBack(SLTNode** pphead);
SLTNode* SListFind(SLTNode* pphead,SLTDataType x);
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在pos前面插入x
void SListErase(SLTNode** pphead,SLTNode*pos);
//删除pos位置的值
SList.c源文件
#include"SList.h"
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void SListPushBack(SLTNode**pphead, SLTDataType x)
{
//SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//newnode->data = x;
//newnode->next = NULL;
SLTNode* newnode = BuySListNode(x);
if(*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}//尾插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}//头插
void SListPopFront(SLTNode** pphead)
{
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
void SListPopBack(SLTNode** pphead)
{
//链表为空
//1个结点
//1个以上结点
if (*pphead == NULL)
{
return;
}
else if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
tail = prev;
}
SLTNode* SListFind(SLTNode* pphead, SLTDataType x)
{
SLTNode* cur = pphead;
while (cur != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
if (pos == *pphead)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* newnode = BuySListNode(x);
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = newnode;
newnode->next = pos;
}
}//在pos前面插入x
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (pos == *pphead)
{
SListPopFront(pphead);
}
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}//删除pos位置的值
test.c源文件
#include"SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);//1 2 3 4
SListPushFront(&plist, 3);//3 1 2 3 4
SListPopFront(&plist);//1 2 3 4
SListPopBack(&plist);// 1 2 3
SLTNode* pos = SListFind(plist, 2);
if (pos)
{
SListInsert(&plist, pos, 30);
}//1 30 2 3
SListPrint(plist);
SLTNode* pos2 = SListFind(plist, 2);
if (pos2)
{
SListErase(&plist, pos2);
}
SListPrint(plist);
}
int main()
{
TestSList1();
return 0;
}
二级指针问题解释
我们先从一个指针部分熟知的swap函数源代码讲起
#include<stdio.h>
void swap(int *x,int *y)
{
int tmp=*x;
*x=*y;
*y=tmp;
}
int main()
{
int a=1;
int b=2;
swap(&a,&b);
return 0;
}
swap函数之所以要传递a,b的地址,是因为函数传递中,实参传递给形参,形参只是实参的临时拷贝,在函数作用结束后拷贝就被销毁,形参的改变和实参没有半毛钱关系,所以要用指针取其地址,从而修改主函数中的变量。
在明白这些概念后我们再来看不带头结点单链表。取出任意一段代码和错误代码比较作为演示。
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}//头插
//以下部分为错误代码
SLTNode* plist = NULL;
SListPushFront(plist, 1);
void SListPushFront(SLTNode* phead, SLTDataType x)
{
SLTNode* newnode = BuySListNode(x);
newnode->next = phead;
phead = newnode;
}//头插error
在这段代码中我们可以看到,plist为一个结构体指针,当用下部分错误形式代码传参时,SLTNode *phead只是SLTNode*plist的一个临时拷贝,本质上是两个不同的指针,所以只是复制了随机位置的指针而已,在函数作用结束后指针就会销毁了,根本找不到phead指向的内容。所以必须要把plist指针的指针到函数中去,也就是二级指针。
而对于带头节点的链表来说,可以使用下半部分代码进行操作,因为在初始化plist时,已经把头结点确定了,而且不会修改他的地址,在复制了带头节点的plist进行函数操作时,复制的头节点和在plist中的头节点指向下一个结点本质是一样的,也就是说带头结点的链表和其形参复制指向的下一个元素都是同一个元素(两个指针指向同一个next)。
总的来说:使用头节点(哨兵位头节点),不需要改变其大小时,可以直接使用一级指针,如果不带头结点(或者需要改变第一个结点的值),则需要二级指针。