本文适用于学了C语言基础,但由于对指针,函数,结构体等知识理解不够透彻,被数据结构课程开门暴击的同学(是我本人)。本文将结合C语言基础知识,详细解释单链表存储结构及基本操作。本人所用的教材是清华大学出版社的《C语言数据结构》,书中多为c++代码,但课程要求所有作业考试的代码均使用C语言,故而本文将使用C语言(改编自课本代码)。
目录
零. 指针相关知识点复习
1.指针的基础理解
先看一段非常简单的代码:
int a=10;
int* p; //声明一个指向整型数据的指针
p=&a; //p的存储单元里写的是a的地址(也就是说指针p指向a)
//这两行可以合起来写成: int* p=&a;
printf(" a=%d, &a=%x\n",a, &a); //输出a的值和地址
printf("*p=%d, p=%x, &p=%x", *p, p, &p); //输出指针p指向的存储地址中的数值,p的值,p自身地址
运行结果:
详细解释(很重要):
int* p 声明了一个指向整型的指针;p=&a 代表着将变量a的存储地址赋给p;*p代表着指向地址为p值(65fe1c)的变量的值。
我们可以把每个存储单元看成一个抽屉,变量的值就是这个抽屉里放的东西,地址就是这个抽屉的编号标签;那么指针变量的“抽屉”同样也有自己的地址和存储内容,只是这个抽屉里放的是其他抽屉的编号标签。“*”代表“指向”,“&”代表取地址。
2.指针与函数
我们先来看一段代码:
一级指针:
bool ValueChange(int* add){
printf("add=%x (*add)=%d",add,*add);
*add=(*add)+1;
return true;
}
int main() {
int a=2;
int* p=&a;
printf("a=%d\n",a);
ValueChange(p); //传入a的地址
printf("\nAfter change, a=%d", a);
return 0;
}
我们传入函数的是变量a的地址,而非a的值,所以函数不是在对参数值进行操作,而是对处于“65felc”这一存储位置的变量本身进行操作。
我们再来看看二级指针:
typedef int* IntPointer;
bool ValueChange(int* add){
printf("add=%x (*add)=%d",add,*add);
*add=(*add)+1;
return true;
}
void PointerChange(IntPointer *ph, int* qh){
printf("\nph=%x, (*ph)=%x, *(*ph)=%d",ph,*ph,*(*ph));
//ph是指针自己的存储地址,*ph是指针的存储内容(即另一变量的地址),*(*ph)就是存储变量的值
*ph = qh;//改变p的指向对象
printf("\nafter change:");
printf("\nph=%x, (*ph)=%x, *(*ph)=%d",ph,*ph,*(*ph));
}
int main() {
/*一级指针*/
printf("1:");
int a=2;
IntPointer p=&a;//等价于 int* p=&a;
printf("a=%d\n",a);
ValueChange(p);
printf("\nAfter change, a=%d", a);
/*二级指针*/
printf("\n\n2:");
int b=6;
IntPointer q=&b;
printf("\n&a=%x,a=%d",&a,a);
printf("\n&b=%x, b=%d",&b,b);
PointerChange(&p,q);
printf("\nafter PointerChange:");
printf("\np=%x, (*p)=%d",p,*p);
return 0;
}
注意:ph是指针自己的存储地址,*ph是指针的存储内容(即另一变量的地址),*(*ph)就是存储变量的值。
在第一段代码中(ValueChange函数及/*一级指针*/之后的代码),我们将变量a的地址传入ValueChange函数,在函数中,我们改变了变量a的存储内容(即*add的值)。
而在这段代码中(PointerChange函数及/*二级指针*/之后的代码),我们将指针p的地址,及变量b的地址传入PointerChange函数,在函数中,我们将p的存储内容(即*ph 的值)改为了b的地址(65fe04)。
通过这两段代码可以看出,一级指针用于指向的变量进行操作(例如:改变指向变量的值);二级指针用于对指针本身进行操作(例如:改变指向对象,由指向a改为指向b)。
3.指针与结构体
结构体相当于自定义的数据类型,在理解结构体指针时,可以类比指向int型数据指针。后文存储结构标题下会有详细解释。
一. 线性表的链式表示
线性表的链式表示:用一组任意的存储单元(可以连续也可以不连续)存储线性表的数据元素
数据元素会在数据域存储其本身信息,在指针域存储其直接后继的存储位置。
数据域 data | 指针域 next |
二. 存储结构
typedef int ElemType;//以存储数据是int型为例
typedef struct LNode{
ElemType data; //数据
struct LNode* next; //指针
}LNode, *Linklist; //LNode *L 等价于 Linklist L
1.struct LNode* next:int* p 代表着声明一个指向整型变量的指针变量,同理,struct LNode* next 则代表着声明一个指向LNode这个结构体类型的指针变量。
2.LNode 与 LinkList:将来我们要声明一个指向LNode类型的指针,我们可以写 LNode* pL;也可以直接写成 LinkList pH; LNode *L 等价于 Linklist L
3.存储结构图解:
三. 基本操作
1. 初始化:创建带头节点的空链表
main 函数中:
Linklist L;
InitList(&L); //将L的地址传入函数
初始化函数:
void InitList(Linklist *pL) //相当于LNode* *L,二级指针可以修改指针本身
{
(*pL)=(Linklist)malloc(sizeof(LNode));//给头结点分配内存空间
if(!(*pL)) exit(1);
(*pL)->next=NULL;//头指针指向头结点 头结点下一位为空
}
注意:初始化需要对指针本身进行修改,故我们使用到二级指针
2. 查找:要获取第i个元素,必须从头节点开始
注意:查找不需要对指针本身进行改动,故只用到了一级指针
bool GetElem(Linklist L, int pos, ElemType* e)//不需要对L本身进行改动
{
Linklist p=L;
int j=0;
while(p&&j<pos)//令p指向第pos个结点
{
p=p->next; j++;
}
if(!p) return false;
*e=p->data; return true;
}
3. 插入操作:<ai, ai+1> --> <ai, e>, <e, ai+1>
bool ListInsert(Linklist* pL, int pos, ElemType e)//头指针,插入位置,插入内容
{
Linklist q=*pL;int j=0;
while(q&&j<pos-1)//令q指向第pos-1个结点
{
q=q->next;
j++;
}
if(!q||j>pos-1){
printf("q is empty? %d j=%d pos=%d\n",!q,j,pos);
return false;
}//参数不合法,pos<1或 pos>表长+1
Linklist s=(Linklist)malloc(sizeof(LNode));
if(!s) exit(1);
s->data=e;//把新元素放进新结点
s->next=q->next; q->next=s;//修改指针,插入结点
return true;
}
step 1: 创建新节点
step 2: 插入新节点
1)令工作指针 q 指向第 pos-1 个节点
(主要讲为啥是 j < pos-1, 非常理解 while 循环的可以不看)
2)修改指针,插入节点
4. 删除
bool ListDelete(Linklist* pL, int pos, ElemType* e)//第pos个元素,用指针e带回
{
Linklist p= *pL;
int j=0;
while(p->next&&j<pos-1) //令p指向第pos-1个节点
{
p=p->next;
j++;
}
if(!(p->next)||j>pos-1) return false;//参数不合法,pos<1或 pos>表长+1
Linklist q=p->next; p->next=q->next;//修改指针
*e=q->data;
free(q);//释放结点空间
return true;
}
删除与插入节点是反过程,可以对应起来一起看
step 1 令工作指针指向第pos-1个节点
step 2 修改指针
step 3 释放空间
5. 销毁
void DestroyList(Linklist *pL)//Linklist类型指向透支着
{
Linklist q;
while(*pL)//销毁以L为头指针的单链表,释放所有结点空间
{
q=*pL;
*pL=(*pL)->next;
free(q);
}
*pL=NULL;
}
OK啦~ 从C语言基础到数据结构入门就结束啦~
之后的内容无论怎么变化,都是基于对结构体,指针等基础知识的理解与应用,跨过这道坎后面的知识就会好理解一些些~
附:有 test 和所有基本操作函数的完整链表代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int ElemType;//以int为例
/*一.线性表的链式表示:存储单元可以连续也可以不连续;
数据域(自身信息)+指针域(直接后继的存储位置)*/
/*二.存储结构*/
typedef struct LNode{
ElemType data; //数据
struct LNode *next; //指针
}LNode, *Linklist; //相当于struct LNode, struct LNode*
//LNode *L 等价于 Linklist L, 可以用于二级指针
/*三.单链表基本操作:1.初始化 2.获取元素 3.插入元素 4.删除元素 5.销毁结构*/
//1.创建带头节点的空链表
void InitList(Linklist *pL) //相当于LNode* *L,二级指针可以修改指针本身
{
(*pL)=(Linklist)malloc(sizeof(LNode));//给头结点分配内存空间
if(!(*pL)) exit(1);
(*pL)->next=NULL;//头指针指向头结点 头结点下一位为空
}
//2.销毁结构
void DestroyList(Linklist *pL)//Linklist类型指向透支着
{
Linklist q;
while(*pL)//销毁以L为头指针的单链表,释放所有结点空间
{
q=*pL;
*pL=(*pL)->next;
free(q);
}
*pL=NULL;
}
//3.获取元素:要获取第i个元素,必须从头节点开始
bool GetElem(Linklist L, int pos, ElemType* e)//不需要对L本身进行改动
{
Linklist p=L;
int j=0;
while(p&&j<pos)//令p指向第pos个结点
{
p=p->next; j++;
}
if(!p) return false;
*e=p->data; return true;
}
//4.插入元素:修改相应指针
bool ListInsert(Linklist* pL, int pos, ElemType e)
{
Linklist s=(Linklist)malloc(sizeof(LNode));
if(!s) exit(1);
s->data=e;//把新元素放进新结点
Linklist q=*pL;int j=0;
while(q&&j<pos-1)//令q指向第pos-1个结点
{
q=q->next;
j++;
}
if(!q||j>pos-1){
printf("q is empty? %d j=%d pos=%d\n",!q,j,pos);
return false;
}//参数不合法,pos<1或 pos>表长+1
s->next=q->next;
q->next=s;//修改指针,插入结点
return true;
}
//5.删除元素
bool ListDelete(Linklist* pL, int pos, ElemType* e)//第pos个元素,用指针e带回
{
Linklist p= *pL;
int j=0;
while(p->next&&j<pos-1)
{
p=p->next;
j++;
}
if(!(p->next)||j>pos-1) return false;//参数不合法,pos<1或 pos>表长+1
Linklist q=p->next;
p->next=q->next;//修改指针
*e=q->data;
free(q);//释放结点空间
return true;
}
/************************************* Testing ****************************************/
int main()
{
Linklist L;//头指针(结构体指针),指向下一个结构体:LNode *L
/*1.初始化*/
InitList(&L);
//2.输入int类型数据,输入-1停止操作
ElemType num; int i=1;
printf("please enter the data end up with -1\n");
do{
scanf("%d",&num);
if(num!=-1)
{
//printf("%d: %d\n",i,ListInsert(&L,i,num));
ListInsert(&L,i,num);
}
i++;
}while(num!=-1);
/*3.将数据9插入第三个节点,检测是否插入成功,并输出链表数据*/
printf("Insert successfully?:%d\n",ListInsert(&L,3,9));
Linklist q=L;
printf("now the Linklist: ");
for(i=0;q->next!=NULL;i++)
{
q=q->next;
printf("%d ",q->data);
}
/*4.删除第四个节点,检测是否删除成功*/
int E=1;
int* e=&E;//指针要初始化;
printf("\nDelete sucessfully?:%d",ListDelete(&L,4,e));
if(ListDelete(&L,4,e)) printf(" The deleted num=%d\n",*e);//输出删除数据
/*5.查找第三个节点数据*/
printf("Get sucessfully?: %d", GetElem(L,3,e));
if(GetElem(L,3,e)) printf(" The third num=%d\n",*e);//输出删除数据
/*6.输出链表所有数据*/
Linklist p=L;
printf("now the Linklist: ");
for(i=0;p->next!=NULL;i++)
{
p=p->next;
printf("%d ",p->data);
}
/*7.销毁*/
DestroyList(&L);
return 0;
}