数据结构之线性链表单链表(含引用语法解释)

声明:

​ 此博客所有内容均基于个人对数据结构的理解,仅供参考,若发现任何错误希望得到指点,衷心希望此内容对您有所帮助!接下来让我们一起来学习数据结构吧!

1链表的构建与解释(方法实现)。

首先我们学习之前必须保证具有一点的C语言基础,整个数据结构几乎是基于结构体和指针,如果对结构体和指针不是很了解,建议先去对结构体和指针有一定的了解。此内容只包括方法实现,并没有线性链表思想。

1.1创建结构体

  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作为头指针。

1.2线性链表的初始化(分配结点空间)

  • 初始化空表(分配结点)
/*初始化空表*/
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”为这段空间的指针。分配的结点空间可以是头结点,也可以是普通结点(这里是头结点)。我们可以利用结点做头结点或者做普通结点用添加数据、暂存数据、转存数据等等,也就是我们所操作的最小单元,我们所有的操作都在其之上。也可以把它看作普通的数组,就是操作方式比较繁琐。

1.3尾插法创建单链表

  • 尾插法函数实现与解释
/*尾插法建立单链表*/
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是尾结点的指针。小结:尾插法操作只涉及到了尾结点,较为简单。

1.4关于取地址符“&”的解释(为什么在函数的参数前添加取地址符(&L)?)

​ 这是关于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的值,那么我们的初始化也是没有意义的。

2对单链表的基本操作

​ 对单链表的基本操作:单链表的遍历、增删查改以及各种特殊操作。我们下面主要是对各个函数的实现以及解释。

2.1单链表的遍历

/*单链表遍历*/
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");
}

2.2计算单链表的长度

/*计算单链表长度*/
int length_Linklist(LinkList L)
{
    int count = 0;   //用count存储单链表的长度
    L->next;         //指向首节点
    while(L){        //封闭指针
	    count++;
	    L = L->next; //向后移位
	}
    return count;
}

​ 计算单链表长度与遍历单链表差不多,只是将打印“ printf("%d ", L->data);”换成了“count++;”,此函数返回值为单链表长度。

2.3单链表的逆置(头插法)

/*单链表逆置*/
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节点成为单链表的第一个节点)。

2.4删除为偶数的节点

/*删除值为偶数的结点*/
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;)。

2.5插入函数和生成非递减单链表

25.1插入元素函数的实现

/*在升序单链表中插入元素,链表仍然有序,插入成功返回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为空表时,则循环体不运行(见循环下注释)。当不为空表时,则进入循环体进行判断,如果运行到最后一个节点时仍未找到合适位置,则跳出循环体,运行下方语句,将此节作为单链表的尾节点。

2.5.2生成非递减单链表

/*创建非递减有序单链表,创建成功返回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;
}

​ 已有插入函数,在创建非递减有序单链表时,只需初始化一个头结点,然后将插入函数放入循环体中,逐个插入。

2.6单链表的合并

  • 两个非递减有序单链表La和Lb合并成一个非递减有序链表Lc
/*两个非递减有序单链表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,我们只需要将尾插法插入改为头插法插入即可(对于头插法我也是花了一定时间才能理解,可能人和人不一样,自己领悟吧,我感觉我是够笨的)。

    2.7将链表La按值分解为偶数表和奇数表

    *链表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;       //此结点为奇数,向后移位 
    	}
    }
    
    

    此函数也不难理解,我们只需判断是否为偶数,然后进行取出操作以及尾插法插入。注释较为详尽,可供参考。

    3总结有感

    ​ 通过这次学习我受益良多,在这里与大家分享一下。这次学习使我对数据类型有了更深的理解,无论是什么数据类型,只要它是数据类型的一种,那么它所声明的变量也一定遵循普通变量的一般规律。就拿C++语法引用为例,LinkList类型的变量L,就遵循一般规律。

你可能感兴趣的:(C++,C)