此博客所有内容均基于个人对数据结构的理解,仅供参考,若发现任何错误希望得到指点,衷心希望此内容对您有所帮助!接下来让我们一起来学习数据结构吧!
首先我们学习之前必须保证具有一点的C语言基础,整个数据结构几乎是基于结构体和指针,如果对结构体和指针不是很了解,建议先去对结构体和指针有一定的了解。此内容只包括方法实现,并没有线性链表思想。
typedef struct Node
{
Elemtype data;
struct Node *next;
}Lnode,*LinkList;
在解释之前必须清楚一个概念:我们创建了一个数据类型,大家都知道都知道的数据类型有int、double…我们创建的数据类型是Lnode型,它包含两种数据类型,也可以说它是这两种数据类型的综合体,它总是“两两”出现的。
typedef是“定义的意思”,例如
typedef int Elemtype;
就是将 int 定义为Elemtype,在编写代码中,Elemtype就可以当作int来用,所以代码中的“Elemtype”可以代表任何数据类型。
typedef struct Node;
上代码是定义一个结构体Node,此处的Node只是一个结构体的标识,结构体真正的名字为Lnode。但标识也有用处,例如“struct Node *next;”,就是在我们创建的结构体内部定义一个此结构体类型的指针,通俗来讲就是这个指针指向Lnode结构体。
Lnode,*LinkList;
最后一行是说明这个结构体的名称是Lnode(可以理解为创建了一个数据类型为Lnode),*LinkList是指向这个结构体的指针类型,通俗来讲就是LinkList这种类型是指向Lnode的指针,例如
LinkList L;
意思就是L是指向Lnode的指针变量。
小结:这只是为创建线性链表(以下称单链表)做准备,创建了一个数据类型,这个数据类型包含数据域(data)和指针域(*next),我们后边会定义一个LinKList类型的变量L作为头指针。
/*初始化空表*/
Status Init_Linklist(LinkList &L)
{
L = (LinkList)malloc(sizeof(Lnode));//分配空间,并由LinkList类型输出。
if(!L) return ERROR; //异常处理,若未分配成功,则返回ERROR(或0)
L->next = NULL; //表明这是一个空表(空节点)
return OK;
}
这是一个初始化函数,所谓初始化就是为单链表结点分配空间,或者说分配一个结点空间。“L = (LinkList)malloc(sizeof(Lnode));”是分配一个Lnode类型的空间,返回值为地址,即“L”为这段空间的指针。分配的结点空间可以是头结点,也可以是普通结点(这里是头结点)。我们可以利用结点做头结点或者做普通结点用添加数据、暂存数据、转存数据等等,也就是我们所操作的最小单元,我们所有的操作都在其之上。也可以把它看作普通的数组,就是操作方式比较繁琐。
/*尾插法建立单链表*/
Status Creat_Linklist(LinkList &L)
{
Elemtype x; //要插入的变量
LinkList p, temp;
Init_Linklist(L); //分配头结点
p = L; //备份头指针,以防头指针丢失
printf("请输入要插入的值,当输入为-1时,停止输入\n");
while(scanf("%d", &x), x != -1){
Init_Linklist(temp); //创建新结点
temp->data = x; //变量存入新结点的数据域
temp->next = NULL;
p->next = temp; //将新结点接到上个结点的尾部,
//(将新结点的指针存入上个节点的指针域)
p = p->next; //操做结点向后移位
}
}
尾插法是比较简单的操作,有助于链式操作的理解和掌握。(Init_Linklist(L);)首先我们分配了一个头结点空间,由于头指针不可丢失,(p = L;)所以操作时用p备份。这是添加结点的操作,我们需要创建新结点(Init_Linklist(temp);)temp将要变量x存入新结点的数据域,且将此结点的指针域设为空。(p->next = temp;)将此指针赋予尾结点的指针域。(p = p->next)保证p是尾结点的指针。小结:尾插法操作只涉及到了尾结点,较为简单。
这是关于C++中的一个语法,叫做引用。对于引用,我查阅一些资料,虽然也没能彻底搞明白,却也从另一个容易理解的角度,获得了自己的一些认识,与大家分享(我们也可以直接百度)。下面我做一些解释,帮助大家理解。首先我们搞清楚“L”是什么?L是LinkList类型的指针变量,LinkList类型的变量纷纷指向Lnode结构体(前面我们已经知道Lnode是一种数据类型,一种包含两部分的数据类型)。下面我们以初始化函数为例对引用进行解释。
/*初始化空表*/
Status Init_Linklist(LinkList &L)
{
L = (LinkList)malloc(sizeof(Lnode));//分配空间,并由LinkList类型输出。
if(!L) return ERROR; //异常处理,若未分配成功,则返回ERROR(或0)
L->next = NULL; //表明这是一个空表(空节点)
return OK;
}
此函始中“L = (LinkList)malloc(sizeof(Lnode));”,明显改变了变量L的值。在学习C语言中,我们都知道传入函数变量是形参,传入只是具体变量的值,在函数中无论值怎么改变,对于此函数外(主函数内)的变量并没有影响。在这里也一样,如果不加取地址符“&”,也不能改变L的值,那么我们的初始化也是没有意义的。
对单链表的基本操作:单链表的遍历、增删查改以及各种特殊操作。我们下面主要是对各个函数的实现以及解释。
/*单链表遍历*/
void Disp_Linklist(LinkList L)
{
LinkList p; //p将指向操作节点(p将为操作节点)
p = L->next; //将第一个节点设为操作节点
while(p) //当p为空时,循环结束(这样操作可以有效的避免指针越界的情况,封闭指针)
{
printf("%d ", p->data); //输出数据域存储的数据
p = p->next; //操作节点向后移位,操作下一个节点
}
printf("\n");
}
单链表的遍历没有什么难于理解,主要是清楚我们对单链表一个一个节点操作的过程。还有一点,关于指针越界问题。指针越界直接导致程序运行失败,所以我们在操作指针向后移位时,最好将它封闭。例如“while§”,当p=NULL时,循环里的操作就无法运行(如果p指向空,再进行移位,就会造成空指针异常,也就是指针越界)。另外为了加深大我们对上述引用的理解,在此多数几句。在此函数中,我们并没有对单链表进行改变,所以不需要添加取地址符。没有添加取地址符,我们就无法改变变量L的值。那么我们就没必要担心丢失头节点L,函数可以改成以下。
/*单链表遍历*/
void Disp_Linklist(LinkList L)
{
L = L->next;
while(L)
{
printf("%d ", L->data);
L = L->next;
}
printf("\n");
}
/*计算单链表长度*/
int length_Linklist(LinkList L)
{
int count = 0; //用count存储单链表的长度
L->next; //指向首节点
while(L){ //封闭指针
count++;
L = L->next; //向后移位
}
return count;
}
计算单链表长度与遍历单链表差不多,只是将打印“ printf("%d ", L->data);”换成了“count++;”,此函数返回值为单链表长度。
/*单链表逆置*/
void Reverse_Linklist(LinkList L)
{
LinkList p, q;
p = L->next; //p指向第一个结点
L->next = NULL;
while(p){ //封闭指针
q = p; //p为操作结点
p = p->next; //将操作p向后移位
q->next = L->next; //将操作结点的指针域指向原来的第一个节点(头结点的后继)
L->next = q; //将操作结点做为头节点的后继
//(将头结点的指针域的指针指向操作节点)
}
单链表的逆置主要运用了头插法,前面我们知道尾插法较为简单,但是头插法理解起来较为困难。首先我们需要两个指针p和q,p为顺寻指针,从第一个节点到最后一个节点保证将全部节点逆置,q则指向操作节点。首先“q->next = L->next;”即节点q的指针域指向第一个节点(第一个节点成为q节点的后继),再者“L->next = q;”即q节点成为头节点的后继(q节点成为单链表的第一个节点)。
/*删除值为偶数的结点*/
void DelEven_Linklist(LinkList L)
{
LinkList p, q;
int len = length_Linklist(L); //计算单链表长度
p = L; //备份头指针
while(len--){ //循环处理次数len
q = p->next; //操作判断结点为q
if(q->data%2 == 0){ //判断是否为偶数
p->next = q->next;//是偶数则是此结点的前驱指向后继
free(q); //释放删除结点的内存
continue; //结束本次循环
}
else
p = p->next; //不是偶数则向后移位
}
删除节点必定要释放内存,最好不要用L变量直接操作,所以所以我们定义了两个变量p和q,p仍为循序指针,为的是遍历所有节点,q则为操作节点。我用单链表的长度做封闭(这样做确实有指针越界的风险,但是我改良了很久,也没有成效。我承认这个函数写的不怎么样,只能如此了,希望大家能有所改进)。主要其实就是一个判断,如果此节点是是偶数,则让此节点的后继成为此节点前驱的后继(p->next = q->next;),再释放掉此节点就行了;如果不是,则跳过此节点(p = p->next;)。
/*在升序单链表中插入元素,链表仍然有序,插入成功返回OK,插入失败返回ERROR*/
Status Insert_Linklist(LinkList L, int x)
{
LinkList p, q, temp;
p = L; //备份头指针
Init_Linklist(temp); //创建新结点
temp->data = x; //将变量存入新结点的数据域
temp->next = NULL; //新结点指针域设为空
while(p->next){ //循环遍历寻找合适位置
q = p->next; //q为对比结点
if(x <= q->data){ //此处默认为升序
temp->next = p->next; //将结点插入其中
p->next = temp;
return OK; //结束函数
}
p = p->next;
}
Init_Linklist(temp); //如果单链表的长度为0,则新结点为单链表的第一个结点
temp->data = x;
temp->next = NULL;
p->next = temp;
return OK;
}
此函数是“一次性”函数,每次可插入一个元素(默认升序),此函数在生成非递减单链表函数应用。此函数满足空表插入,当L为空表时,则循环体不运行(见循环下注释)。当不为空表时,则进入循环体进行判断,如果运行到最后一个节点时仍未找到合适位置,则跳出循环体,运行下方语句,将此节作为单链表的尾节点。
/*创建非递减有序单链表,创建成功返回OK,创建失败返回ERROR*/
Status CreatOrder_Linklist(LinkList &L)
{
LinkList p, q, temp;
Elemtype x;
Init_Linklist(L); //为单链表分配空间(创建头结点)
L->next = NULL; //单链表的长度为0
printf("请输入要插入的值,当输入为-1时,停止输入\n");
while(scanf("%d", &x), x != -1) //输入变量
Insert_Linklist(L, x); //生成单链表
return OK;
}
已有插入函数,在创建非递减有序单链表时,只需初始化一个头结点,然后将插入函数放入循环体中,逐个插入。
/*两个非递减有序单链表La和Lb合并成一个非递减有序链表Lc*/
void MergeAscend_Linklist(LinkList La, LinkList Lb, LinkList &Lc)
{
LinkList pa, pb, pc,temp;
Init_Linklist(Lc);
pa = La;
pb = Lb;
pc = Lc;
pa = pa->next; pb = pb->next; //pa,pb指向第一个结点
while(pa && pb){ //(封闭指针)循环尾插法
if(pa->data <= pb->data){ //判断较小的值
pc->next = pa; //将pa以尾插法插入Lc中
pa = pa->next; //pa向后移位
pc = pc->next; //pc指向刚才插入的节点等待下一次插入
pc->next = NULL; //pc所指向的节点指针域置空
}
else{
pc->next = pb; //将pa以尾插法插入Lc中
pb = pb->next; //pb向后移位
pc = pc->next; //pc指向刚才插入的节点等待下一次插入
pc->next = NULL; //pc指向的节点指针域置空
}
}
if(pa) //若La剩余,将La剩余元素插入Lc尾,否则将Lb剩余元素插入Lc尾
pc->next = pa;
else
pc->next = pb;
}
注释较为详尽,而且对于两个非递减有序单链表合并生成一个非递减有序单链表,还是相对简单的。我们只需比较La中与Lb中元素逐个以尾插法插入Lc中。 若La元素小于Lb元素,则让La元素以尾插法插入Lc中,La对比元素位置向后移位,Lb对比元素位置不变,反之亦然。当La或者Lb任意一个所剩元素(节点)数为零时,跳出第一个循环体。剩下的两个循环体则将La或者Lb中剩余元素可直接插入Lc尾。
两个非递减有序单链表La和Lb合并成一个非递增有序链表Lc
/*两个非递减有序单链表La和Lb合并成一个非递增有序链表Lc*/
void MergeDescend_Linklist(LinkList La, LinkList Lb, LinkList &Lc)
{
LinkList pa, pb;
Init_Linklist(Lc); //初始化单链表Lc
pa = La->next;
pb = Lb->next;
while(pa && pb){ //指针封闭,循环头插法
if(pa->data <= pb->data){ //将较小的值插入进去
La->next = pa->next; //此(第一个)结点的后继设为头结点后继
pa->next = Lc->next; //将Lc``(头指针)的后继设为pa结点的后继
Lc->next = pa; //将pa设置为Lc(头指针)的后继
}
else{
Lb->next = pb->next; //拿出此结点,并将后一个结点作为头结点的后继
pb->next = Lc->next; //将Lc(头指针)的后继设为pa结点的后继
Lc->next = pb; //将pa设置为Lc(头指针)的后继
}
pa = La->next; //指向第一个结点
pb = Lb->next;
}
while(pa){ //将La中剩余的插入Lc
La->next = pa->next; //此(第一个)结点的后继设为头结点后继
pa->next = Lc->next; //将Lc的第一个结点设为此结点的后继
Lc->next = pa; //将pa设为Lc的第一个结点(设为头结点的后继)
pa = La->next;
}
while(pb){ //将Lb中剩余的插入Lc
Lb->next = pb->next; //此(第一个)结点的后继设为头结点的后继
pb->next = Lc->next; //将Lc的第一个结点设为此结点的后继
Lc->next = pb; //将此结点(pa)设为头节点的后继
pb = Lb->next;
}
}
比较两个非递减有序单链表La和Lb合并成一个非递减有序链表Lc,我们只需要将尾插法插入改为头插法插入即可(对于头插法我也是花了一定时间才能理解,可能人和人不一样,自己领悟吧,我感觉我是够笨的)。
*链表La按值分解成两个链表,La全部为奇数,Lb全部为偶数*/
void Split_Linklist(LinkList La, LinkList &Lb)
{
LinkList pa, pb, qa;
Init_Linklist(Lb); //初始化链表Lb
qa = pa = La; //备份La头节点
pb = Lb; //备份Lb头节点
while(qa = pa->next, qa){ //qa为判断依据,当qa为空时,循环体结束
if(qa->data%2 == 0){ //判断为偶数
pa->next = qa->next; //将qa的后继设为qa的前驱的后继(将qa节点从La中取出)
pb->next = qa; //将qa设为Lb尾结点的后继
pb = pb->next; //将pb指向尾结点
pb->next = NULL; //将pb(尾结点)的指针域为空
}
else
pa = pa->next; //此结点为奇数,向后移位
}
}
此函数也不难理解,我们只需判断是否为偶数,然后进行取出操作以及尾插法插入。注释较为详尽,可供参考。
通过这次学习我受益良多,在这里与大家分享一下。这次学习使我对数据类型有了更深的理解,无论是什么数据类型,只要它是数据类型的一种,那么它所声明的变量也一定遵循普通变量的一般规律。就拿C++语法引用为例,LinkList类型的变量L,就遵循一般规律。