线性结构--离散存储 链表讲解

数据结构大体成上可以分成两种:

1. 线性结构.
2. 非线性结构( 树,图)

1. 什么是线性结构
       大概上可以这样定义: 加入所有的节点可以用一条直线连接起来. 就是线性结构...


2. 线性机构也可以分成两种:
       1) 连续存储 (数组)
             也就是指每1个节点在物理内存上是相连的.

       2) 离散存储(链表)
              节点在物理内存上并不一定相连, 而是利用指针来关联.

       这篇文章主要讲的第二种.

          


3. 链表的定义

        1.多个节点离散分配.

        2.彼此通过指针相连.

       3.每个节点只有1个前驱节点, 1个后续节点.

       4.首节点没有前驱节点, 尾节点没有后续节点.


       其中第1 2点也适用于树和图,   后面3 4点就用于区别树和图了.



4.一些专业用语解析:

      1) 首节点:

                   第一个存放有效数据的节点.

      

      2)节点:

                   最后一个存放有效数据的节点.尾节点的尾部指针为空


       3) 节点:       

                   第1个有效节点之前的那个节点.

                    头节点并不存放那个有效数据,  但是头节点跟后面每个节点的数据类型是一样的.

                   加头节点的目地主要是为了方便对链表的操作.


        总之, 要注意头节点并不是首节点,  而是首节点前面的1个不存放有效数据的节点, 用于方便链表操作.


       4)头指针:        

                  指向头节点的指针, 也是头节点的地址

     

       5)指针:        

                  指向节点的指针, 也是节点的地址


                    具体可以参考下图所


 

5.确定1个链表所必须的几个参数

        首先: 如果是确定1个数组就必须知道数组的头指针 和 数组的长度.

        但是链表是可以根据每个节点的尾部指针一直遍历下去, 如果遇到1个节点的尾部指针是空, 就认为它是尾节点.


        如果希望通过1个函数来对数组进行处理,例如打印数组的函数,或者数组排序的函数,就要接受数组的头部指针,和数组的长度.

       但是如果希望通过1个函数来对链表进行处理,

       只需要1个参数:

                 就是链表的头部指针啦,  因为链表的长度等其他信息都可以推算出来的.



6. 如何用代码表示1个节点.

        首先, 要明确两点:

            1) 1个链表中每个节点的数据类型都是一样的.

            2) 每个节点的数据都分两部分(域), 1是数据部分,  2是尾部指针.


             所以结构体的数据类型不能是常规的数据类型(int /long / char 等) , 必须是结构体, 因为结构体才能可能有多个成员嘛.

             

             而每个节点的尾部指针指向的是下1个相同类型的节点, 所以尾部指针的类型和结构体的类型是必须一样的.

struct person{
		int id;
		char name[16];
		struct person * pnext;
	};
	
	typedef struct person PERSON;

               如上面代码,这样就定义了1个 简单的PERSON 类型的节点.


7.链表的分类:

       7.1 单链表和双链表

                   单链表中每1个只有1个指针域, 例如上面代码定义的就是单链表的结点

                   双链表中每个指针有2个指针域,   其中1个指针域指向下1个节点,  另1个指向前1个结点.


          

       7.非循环链表和 循环链表

                     这个容易理解,  如果尾节点的尾部指针指向首节点, 那么它就是1个循环链表.



8.链表的一些算法:

       所谓算法就是对链表的一些操作了.

       包括和很多种, 当然我只会实现几种最基本常用的算法:

               新建1个链表

               添加1个节点到尾部

               插入节点

               删除节点

               遍历

               查找某个节点

               清空

               销毁

               求长度

               排序

             


       下面会对这些算法进行讲解和代码实现




9. 1个单链表(非循环)容器的简单c语言实现例子



9.1. 编写头文件.

      因为c语言并不面向对象语言, 所以我们不能将编写好的链表容器作为1个类库, 所以要写个头文件,  那么别的文文件引用了这个头文件, 就可以使用这个链表容器了.


     头文件代码如下:

     linklist1.

//注意bool_me.h 这是一个简单定义布尔类型 宏的头文件.

#include "bool_me.h"
#ifndef __LINKLIST1_H_H
#define __LINKLIST1_H_H

	struct person{
		int id;
		char name[16];
		struct person * pnext;
	};
	
	typedef struct person PERSON;

	struct Link_person{   //just a struct for linklist,  it is not a node of linklist
		PERSON * phead;   //address of the Headnode of linklist
		PERSON * ptail;  // address of the tailnode, yes it's not a neccssity, but it can make the operation easily
		int len; //numbers of the nodes which contains the userful data.it's not a neccssity, but it can make the operation easily
		BOOL is_inited; // judge whether the linklist is inited
	};
		

	typedef struct Link_person LINKPERSON;

	//init a new block()
	PERSON * person_new(int id,char *pname);

	//init a Linklist
	LINKPERSON * link_create(int len);

	//judge whether the linklist is empty
	BOOL link_is_empty(LINKPERSON * pLink);

	//add a node to the tail of Linklist
	BOOL link_add(LINKPERSON * plink, PERSON * pnode);
	
   //traverse the linklist to print all of the node of linklist;
	void link_traverse(LINKPERSON * pLink);

   //insert a node behind another node
	BOOL link_insert(LINKPERSON * pLink, PERSON * pBefore, PERSON * pnode);
	
   //insert a node behind another node
	BOOL link_insertbyindex(LINKPERSON * pLink, int index, PERSON * pnode);

   //remove a node from the linklist
	BOOL link_remove(LINKPERSON * pLink, PERSON * pnode);

   //delete a node from the linklist, and free the memory space of the node
	BOOL link_delete(LINKPERSON * pLink, int index);
	
	//get a the index of a node, if not existed, return -1
	int link_getindex(LINKPERSON * pLink, PERSON * pnode);
	
	//get the length of Linklist
	int link_getlength(LINKPERSON * pLink);	

	//clear a Linklist
	BOOL link_clear(LINKPERSON * pLink);
 
	//destroy a Linklist
	BOOL link_free(LINKPERSON * pLink);
	
	//sort by id
	void link_sort(LINKPERSON * pLink);

	//get a node from linklist by index
	PERSON * link_getnode(LINKPERSON * pLink, int index);

#endif






              讲解下,  这里我定义了两个结构体, 其中第1个结构体PERSON就是链表的节点类型

              而第2个结构体LINKPERSON是1个链表本身的类型, 里面包含了链表的第1个(头节点)结构体的地址.  通常这个头节点是不存放有效数据的, 上面提过了.

              第2个结构体里面存放了一些链表的关键信息, 虽然这些关键信息例如, 长度, 尾节点地址等都可以由头节点地址推算出来, 但是毕竟遍历推算是1个很浪费cpu时间的行为, 牺牲一些内存空间来方便运算啦


              下面定义了若干提供给其他程序是使用的函数,  当然我这个例子是1个极其基本简单的例子, 也只会实现一些基本的功能的函数啦.             



9.2. 处理错误函数voidlink_error();

         , 虽然只是1个简单的实现例子, 但是国际惯例,还是尽量写的专业一些吧..


        代码如下:

linklist1.c           //后面的函数都写在这个文件里

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "linklist1.h"

static void link_error(const char * pErr);


static void link_error(const char * pstr){
	printf("%s\n", pstr);
	exit(-1);
}


9.3. 创建1个新的节点函数 PERSON* person_new(int id,char* pname)

        为什么要专门写1个函数来创建node? 

        直接 PERSONnode1; 这样不就创建出1个结构体节点了吗?


         没错, 但是这样创建的结构体变量是静态变量, 也就代表它所占的内存不能重复使用,  而且当这个节点被移除时, 它所占的内存也不能被清空啊.

        所以我会单独写1个函数来创建1个新的节点


        逻辑:

         1.定义1个结构体指针

         2. 为这个指针分配1块足够的内存

           4. 结构体的各成员赋值, 尾部指针为空指针.

        3. 返回这个指针所指向的地址.


        代码如下:


PERSON * person_new(int id, char * pname){
	PERSON * pnode;
	pnode = (PERSON *)malloc(sizeof(PERSON));
	if (NULL == pnode){
		link_error("fail to assign memory to PERSON");
	}

	pnode->id = id;
	strcpy(pnode->name, pname);
	pnode->pnext = NULL;
	return pnode;
}


9.4.初始化(新建)1个链表函数 LINKPERSON *link_create(int len)

         初始化1个链表,  其实这个逻辑不难理解,    至于为什么有1个参数len?   其实就是方便用户新建1个链表给这个链表分配数量为len的有效节点, 当然len 可以设成0啦, 这样这个链表就只有1个头节点了.

         无非分如下若干步:

         1.新建1个链表类型(自定义 LINKPERSON)指针

         2.动态给这个指针分配1个内存.

          3. 动态分配1个头节点, 地址赋给 上面的链表类型的第1个成员plist,  这个成员就指向头节点的地址. 头节点不存放有效数据, 但是尾部指针设为空

          4.根据len参数动态分配若干个 有效节点, 挂到头节点后面.

           5. 给链表类型的其他成员赋值

         6. 最后返回这个链表类型的地址.


          还是有点复杂啊..


代码如下:

       

LINKPERSON * link_create(int len){
	LINKPERSON * pLink = (LINKPERSON *)malloc(sizeof(LINKPERSON));
	if (NULL == pLink){
		link_error("fail to assign memory to LINKPERSON");
	}

	PERSON * phead = (PERSON *)malloc(sizeof(PERSON));
	if (NULL == phead){
		link_error("fail to assign memory to headnode");
	}

	phead->pnext = NULL;
	pLink->phead = phead;
	pLink->ptail = phead;
	pLink->len = 0;
	pLink->is_inited = TRUE;

	if (len==0){
		return pLink;
	}

	int i;
	char * name[16];
	PERSON * pnode;
	for(i=0;i<len;i++){
		char name[16];
		sprintf(name,"node%d",i+1);
		pnode = person_new(i+1,name);
		if (FALSE == link_add(pLink,pnode)){
			link_error("fail to add nodes!");
		}
	}

	return pLink;
}

       从代码可以见到, 无论初始长度len是否为0 我都会有定义1个头节点,  然后根据len的长度循环将存放数据的有效节点挂到链表的尾部.

       新建1个节点用的是 person_new() 函数, 这个函数上面写过了.

       而将1个节点挂到链表尾部我用的是link_add() 函数, 这个函数头文件也提到的, 下面就会讲解如下实现这个函数.



9.5.将1个节点添加到链表尾部末尾函数 BOOL link_add(LINKPERSON * pLink, PERSON * pnode)

      首先, 这个要添加节点最好是动态分配的, 也就是说用我上面的person_new() 生成的. 否则当移除这个节点就无法释放它的内存了.

      而添加1个节点到链表尾部逻辑上是很简单的..

       1. 链表的尾节点的尾部指针指向这个要添加的节点

        2.这个节点的尾部指针设为NULL.

       3. 链表成员len+1


        但是实际上还是有另外需要注意的问题.

       a. 就是如何获取尾节点?  

                这个当然可以用头节点逐个推算出来了..   但是我文件提过, 为了方便操作, 我会把尾节点的地址也保存在链表类型的结构体成员中..


       b.判断 链表内是否已经有这个节点. 

                这个就跟数组不同了. 数组是可以存放相同的数据的,例如Arr_add(10),这个函数可以重复执行.

                链表呢, 因为链表是由节点组成的, 而唯一标识节点的是节点的地址.

               假如链表添加1个节点, 但是这个节点的地址已经在这个链表中的话就出现下图中的问题了:


                     如下图:


          所以这个函数会判断下这个节点是否在链表中, 还是要遍历一次啊..  有更好方法的可以告诉我..

代码如下:



BOOL link_add(LINKPERSON * pLink, PERSON * pnode){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (NULL == pnode){
		printf("pnode is empty!\n");
		return -1;
	}
	
	//judge whether pnode is existed in the linklist already
	if (link_getindex(pLink,pnode) > -1){
		printf("the node is existed in linklist already!\n");
		return FALSE;
	}

	pLink->ptail->pnext = pnode;
	pnode->pnext = NULL;
	pLink->ptail = pnode;
	pLink->len++;
	return TRUE;
}


可以见到, 我并没有每次都遍历求出 尾节点的地址, 而是把尾节点地址放到 链表类型的1个成员中, 方便操作啊~


上面所以说遍历了1次, 似乎因为执行力获取节点序号函数  link_getindex(),如果这个节点不存在, 则返回0. 下面就是这个函数的写法.


9.6.获取1个节点在链表中的位置.    intlink_getindex(LINKPERSON * pLink,PERSON * pnode)

       注意这个函数也用与判断节点是否存于链表中,  如果存在就返回位置index, 不存在就返回-1.

       逻辑上也很清楚,  冲首节点起(不是头节点), 逐个判断, 直到到达尾节点, 这个过程不需要关心链表的长度.

       而且这个遍历不包括头节点,所以如果把头节点作为参数, 一样会返回 -1,因为头节点不是存放数据的有效节点.


        代码如下:


int link_getindex(LINKPERSON * pLink, PERSON * pnode){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (NULL == pnode){
		printf("pnode is empty!\n");
		return -1;
	}

	PERSON * pn = pLink->phead;
	int i=0;
	while (NULL != pn->pnext){
		if (pn == pnode){
			return i;
		}
		i++;
		pn = pn->pnext;
	}

	return -1;
}


 

9.7. 判断链表是否阿为空 BOOL link_is_empty(LINKPERSON * pLink).

        这个就毫无难度啦,只需判断链表内是否只有头节点就ok了.

         当然, 为了方便操作, 也可以判断 链表成员len 是否等于0

BOOL link_is_empty(LINKPERSON * pLink){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (NULL == pLink->phead->pnext){
		return TRUE;
	}

	return FALSE;
}


9.8. 遍历输出链表函数. void link_traverse(LINKPERSON * pLink)

        这个要注意的是, 实际上就是遍历链表, 然后逐个输出节点.  

          只需定义1个指针,首先指向首节点(头节点的下1个), 然后输出.

          再把这个指针指向被输出的指针的后1个, 继续输出,直到尾部指针为空(尾节点).

         所以这个过程根本不关心链表的长度的.

         

void link_traverse(LINKPERSON * pLink){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (TRUE == link_is_empty(pLink)){
		printf("the linklist is emptyi!");
		return;
	}

	PERSON * pnode;
	pnode = pLink->phead;  //phead

	while(NULL != pnode->pnext){
		pnode = pnode->pnext;
		person_print(pnode);
	}
	return;
}


9.9. 打印1个节点的函数. void person_print(PERSON * pnode)

   就是上面用的person_print(pnode)啦, 太简单不讲解


static void person_print(PERSON * pnode){
	printf("id is %d, name is %s\n", pnode->id, pnode->name );
}


9.10. 根据序号获取1个节点函数 PERSON * link_get(LINKPERSON * pLink, int index)

       相对来讲,这个函数就跟简单了。

       首先, 判断参数 index 的范围, 如果少于0 , 或者大于链表当前个数-1 , 则返回1个空指针。

       链表的个数怎么求?  这时我链表类型的len成员就发挥作用了..虽然作用意义不是很大.

       然后根据index的值  遍历若干次就得到想找的节点地址啦。


       代码如下:

PERSON * link_getnode(LINKPERSON * pLink, int index){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if ( index < 0 || index > (pLink->len -1)){
		printf("index is over the limit!\n");
		return NULL;	
	}
	
	int i;
	PERSON * pnode = pLink->phead;	
	for( i=0;i <= index ;i++){
		pnode = pnode->pnext;
			
	}  	

	return pnode;
}


9.11.插入1个节点到另1个节点之后的函数 link_insert(LINKPERSON *pLink, PERSON * pBefore, PERSON * pnode);

     通常来讲,  链表是整个数据结构的重点,  而插入节点的操作就是链表的重点..

      讲下原理,   假如要将参数中的 pnode 插入到 pBefore的后面.

     假如原来的pBefore 的后面是 pAfter.

     那么pBefore->pnext == pAfter   这个很简单.  

       而现在要把pnode 放到 pBefore 和pAfter 之间.  则pnode 在 pAfter前面

        所以要执行:

                

pnode->pnext = pBefore->pnext         //相当与 pnode->pnext = pAfter

          

         然后pBefore 的后面是pnode, 所以再执行

pBefore->pnext = pnode



        就完成插入了.



         那么对于这个函数, 逻辑上包括如下几点:

        0. 判断pBefore 和 pnode 不是空指针

        1. 判断 pBefore 是否存在于链表中, 如果不存在, 返回false.

        2. 判断 pnode 是否已经存在于链表中, 如果是, 返回false.

        3. 把pnode 插到pBefore 后面

        4. 如果pnode 是最后1个节点.  则pLink的成员 ptail = pnode.

        5. pLink->len++

        6. return true.


代码如下

BOOL link_insert(LINKPERSON * pLink, PERSON * pBefore, PERSON * pnode){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (NULL == pBefore || NULL == pnode){
		printf("pBefore or pnode is empty!\n");
		return FALSE;
	}

	if (0 > link_getindex(pLink,pBefore)){
		printf("pBefore is not existed in linklist!\n");
		return FALSE;
	}

	if (-1 < link_getindex(pLink,pnode)){
		printf("pnode is existed in linklist already!\n");
		return FALSE;
	}

	pnode->pnext = pBefore->pnext;
	pBefore->pnext = pnode;

	if (NULL == pnode->pnext){
		pLink->ptail = pnode;
	}

	pLink->len++;
	return TRUE;
}





9.12. 插入1个节点到制定链表的位置link_insertbyindex(LINKPERSON *pLink, int index, PERSON * pnode);

        这个函数是把1个节点插入到链表的第 index  个节点后面(index 由0(首节点开始));

        这个需要做的事情如下:


        就是调用上面的函数啦 

         link_insert(pLink, link_getnode(index), pnode);         ...


       但是假如  index 不是一个有效的数据, 比如超出了链表范围? 

       那么 link_getnode(index) 就会返回空指针,  而 link_insert 函数接收到空指针参素就会返回FALSE啊~


         代码如下:

BOOL link_insertbyindex(LINKPERSON * pLink, int index, PERSON * pnode){
    if (TRUE != pLink->is_inited){
        link_error("the linklist is not inited yet");
    }

    return link_insert(pLink, link_getnode(pLink, index), pnode);
}


9.13.将1个节点移除出链表BOOL link_remove(LINKPERSON * pLink, PERSON *pnode);

      见到我的头文件中又有remove又 有delete  其实他们作用并不相同,

       首先,  remove 函数只会将节点pnode 移链表, 不会释放这个节点的内存(free), 因为用户很可能还会将这个放到链表的其他地方或者放入另1条链表.


       而delete 函数直接会将 具体位置的节点移除链表后,并销毁 , 释放出内.


       至于怎样移除出1个节点pnode?

         1. 如果节点不再链表中,  返回FALSE

      2.pnode 前1个节点指向 pnode->pnext

       3.如果前1个节点的pnext 是空, 则带代表pnode 是1个尾节点把尾节点移除后,  pLink的成员ptail 的值改成pnode的前1个节点


      问题来了, 怎样找到pnode 的前1个节点 pBefore, 如果是双链表, 直接可以由指针找到, 但是这个是链表..


      当然, 可以先用link_getindex(pnode) 获得 pnode 的 index,  再用link_getnode(index-1) 获得 pBefore,但是这样就执行两次遍历了, 为了性能着想, 还是直接遍历吧..


       代码如下:

BOOL link_remove(LINKPERSON * pLink, PERSON * pnode){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (NULL == pnode){
		printf("pnode is empty!\n");
		return -1;
	}

	PERSON * pBefore = pLink->phead;
	while (NULL != pBefore->pnext ){
		if (pBefore->pnext == pnode){
			pBefore->pnext = pnode->pnext;
			if (NULL == pBefore->pnext){ // pnode is ptail;
				pLink->ptail = pBefore;
				pLink->len--;
				return TRUE;
			}
		}

		pBefore = pBefore->pnext;
	}

	return FALSE; //pnode is not existed in the linklist before.
}


9.14将1个节点移除出链表并释放内存BOOL link_delete(LINKPERSON *pLink,int index);

      好吧, 这个函数实际上就是上面那个remove 函数, 然后再手动释放内存..

       逻辑如下:

       1. 利用link_getnode() 函数获得要remove 的节点

      2. 利用link_remove 移除这个节点.

       3. 手动释放内存.


       代码如下:

BOOL link_delete(LINKPERSON * pLink, int index){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	PERSON * pnode = link_getnode(pLink, index);
	if (NULL == pnode){
		return FALSE;
	}

	if (TRUE == link_remove(pLink, pnode)){
		free(pnode);
		return TRUE;
	}

	return FALSE;

}

9.15  清空1张链表 BOOL link_clear(LINKPERSON * pLink);

   清空链表的意识就是把链表所有的节点移除?

    那么可以循环执行 link_remove吗? 

   执行一次link_remove 就遍历1次啊...


    其实清空1张链表 根本不需要遍历:

    直接将头节点的尾部指针设为NULL 就ok啦!


代码如下:

BOOL link_clear(LINKPERSON * pLink){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	pLink->phead-> pnext = NULL;
	pLink->ptail = pLink->phead;
	pLink->len=0;
	return TRUE;
}



9.16 销毁1张链表, BOOL link_clear(LINKPERSON * pLink);


  这个函数跟上面完全不同啊, 没那么简单~

   首先这个函数分两步,  首先释放所有节点的内存,

  然后释放这个链表类型(结构体)指针本身.


   第2步很简单  直接free(pLink) 就ok了,  问题如何执行第一步呢?

    循环去link_delete吗?  问题每执行一次link_delete 就执行1此 link_remove里面每次都遍历1次啊..


   为了避免渣性能, 还是手动循环去释放节点吧..

     这里有个问题了, 到底是从首节点开始释放内存?  还是由尾节点开始释放?

     如果从尾节点开始释放,  你把尾节点释放后, 怎么找前1个节点? 除了遍历还真没有办法..


      从首节点释放后, 怎么找下1个节点?? 这个就简单啊, 释放首节点前保存它的部节点指针就是了.

      所以我们应该从首节点释放


     代码如下:

BOOL link_free(LINKPERSON * pLink){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	PERSON * pnode = pLink->phead;
	PERSON * pAfter =pnode->pnext;

	//printf("free pnode which id is %d\n",pnode->id);
	free(pnode); //free phead
	while(NULL != pAfter){
		pnode=pAfter;
		//printf("free pnode which id is %d\n",pnode->id);
		pAfter = pnode->pnext;
		free(pnode);
	}

	free(pLink);
	return TRUE;

}



9.17链表排序算法(根据id成员的值) void link_sort(LINKPERSON * pLink)

    当然这里还是讲解下最简单的冒泡排序法了,  

     冒泡排序法在对于数组来讲是十分容易实现的, 基本上都能背出来了~

     代码如下:

int i,j,m;

for (i=0; i<len - 1; i++){
        for(j=i+1;j<len; j++){
                if (a[i] > a[j]){
                        m = a[i];
                        a[i]=a[j];
                        a[j]=m;
                }
        }
}

原理就是循环求出数组最小的值的元素, 放在最左边, 然后求出第二小的元素, 放在数组第2位.....

这里关键就是每比较一次, 如果左边的值大于右边的值

这个两个元素就会互相交互它们的值.



对于链表来讲, 实现原理也差不多的..


1.

数组中用i, j两个变量来存储要比较的元素的位置.

那么在链表中, 我们可以定义2个指针pPre, pAfter来存储要比较节点的位置


2.

数组中i的初始是第1个元素, 所以i=0 开始

对于i的每1个值.   j初始话是i后面1个元素. 所以j从 j=i+1 开始


那么对于链表来讲. pPre就从首节点开始,  所以pPre= phead->pnext  (phead 是头节点)

pAfter 从第pPre的后1个节点开始, 所以pAfter= pPre->pnext 开始.


3.

数组中每执行1次循环, i 或j的值加1, 表示执行她们的下1个元素


链表中, 每执行1次循环. pPre 或 pAfter的地址指向下1个节点. 所以p=p->pnext


代码如下:

void link_sort(LINKPERSON * pLink){
	if (TRUE != pLink->is_inited){
		link_error("the linklist is not inited yet");
	}

	if (pLink->len < 2){
		return;
	}

	PERSON * pPre; //phead
	PERSON * pAfter;

	PERSON * pPB; //used to save the node address which before the pPre
	PERSON * pAB; //used to save the node address which before the pAfter
	PERSON * m;

	for (pPB=pLink->phead,pPre=pLink->phead->pnext; NULL != pPre->pnext; pPB=pPre,pPre=pPre->pnext){
		for (pAB=pPre,pAfter=pPre->pnext; NULL != pAfter; pAB=pAfter,pAfter=pAfter->pnext){
			if (pPre->id > pAfter->id){
				link_exchange(pPB,pAB);
				m = pPre;
				pPre = pAfter;
				pAfter = m;
			};
		}
	}
}

注意上面, 我还定义多两个指针, 专门指向, pPre 和 pAfter 的前1个节点地址,

就是pPB->pnext == pPre        pAB->pnext ==pAfter


为什么这样做, 是因为单链表中无法由1个节点求出上1个节点的地址(只能根据尾部指针得到下1个节点的地址)


在数组中, 

如果比较一次  a[i] > a[j]  之后在就会交换它们的值,


链表比较 pPre->id >pAfter->id 后,   当然也可以互相交换他们的id值,   

例如

int m=pPre->id;
pPre->id = pAfter->id;
pAfter->id =m;

char n[16]=pPre->name;
pPre->name=pAfter->name;
pAfter->name=n;


但是对于1个节点来讲, 地址没有变化, 但是成员值变化了.

实际应用中  , 大部分希望1个节点里面的成员值不会产生变化, 而是节点本身在链表中的位置变化

而且当1个节点的成员非常多时,  交换全部成员的值的成本往往大于交换他们在链表中位置的成本.


我用了

link_exchange(pPB,pAB);
这个函数来交换pPre, 和pAfter的地址,

但是注意传的参数是他们的上1个节点, 而是他们本身.

为什么呢, 下面会讲解这个函数.


假如 pPre指向的是链表第2个节点,  pAfter 指向的是第4个节点

那么交换节点后,  pPre 就指向链表中第4个节点了,  而pAfter 指向第2个节点了.  因为他们指向的节点在链表中的位置换了嘛...

所以根据冒泡排序法的原理, pPre 要指向前面的节点, 而pAfter 要指向后面的节点.


所以我们还要把 pPre 和 pAfter所指向的地址互换.  让pPre 指回第2个节点. 而pAfter 指回第4个节点.

所以要执行

m = pPre;
pPre = pAfter;
pAfter = m
来交换他们的地址啊.



9.18链表交换两个节点的下1个节点函数BOOL link_exchange(LINKPERSON*pPB, LINKPERSON * pAB)

     当然, 可以先执行link_remove 再 link_insert到适当的位置达到目地, 但是就执行了若干次无谓的遍历动作, 不推荐!


     注意的是, 这个函数交换的不是pPB 和 pAB的位置,  是交换 pPB->pnext 和 pAB->pnext 的位置.

     为什么要这样呢, 看到后面就明白了.


首先图解交换两个节点位置要做的操作:


假如我要交换下图中节点2 和 节点4 的位置.


1.首先节点2的前1个节点1的尾部指针指向节点4




2.然后节点4原本的前1个节点3的尾部指针指向节点2

'


3.然后节点2的尾部指针指向节点4的尾部指针地址(节点5)


4.然后节点4的尾部指针指向节点2原本尾部指针地址(节点3)




好了, 经过上面4部后就完成节点2和节点4的位置交换了, 原来顺序是1 2 3 4 5,     后来根据指针就是1 4 3 2 5啦


经过总结, 可以发现上面4步实际上可以总结成为2步:

1. 交换节点2和节点4的前1个节点,     也就是节点2 和 节点4 的前1个节点的尾部指针的值互换

2. 交换节点2和节点4的后1个节点 ,    也就是节点2 和 节点4 的尾部地址的值互换


第2步很简单 

无非就是  p2->pnext  和 p4->pnext 互换

问题来了,

第1步怎么实现呢,   因为根据p2 和 p4 无法知道 p1 和 p3的地址啊,   单链表只能单向求1下1个节点地址,  而不能往前退的.

所以!

'我们在这个函数中, 参数不要定为 p2 和 p4 ,  而是把他们的前1个节点  p1 和  p3 作为参数传入来

那么

p2==p1->pnext

p4==p3->pnext 


那么第1步   把 p1->pnext 和 p3->pnext 的值互换就ok啦

第二步呢,   因为p1->pnext 和  p3->pnext 的值互换过了.

所以p1->pnext 就是p4啊    p3->pnext 就是p2啊   ,  而我们上面见过第2步是 p2->pnext 的值和 p4->pnext的值互换

所以就是 p3->pnext->pnext 的值 和 p1->pnext->pnext 的值互换啊!


代码如下:

static void link_exchange(PERSON * pPB, PERSON * pAB){
	//this function will exchange the place of the next nodes of pPB and pAB
	//if pPB->pnext == pPre ; pAB->pnext == pAfter
	//after exchange, pPre and pAfter will exchange their index in the linklist

	PERSON * m;
	//first , exchange their Pre node;
	m = pPB->pnext;
	pPB->pnext = pAB->pnext;
	pAB->pnext = m;

	//then . exchange their next node;
	m=pPB->pnext->pnext;
	pPB->pnext->pnext = pAB->pnext->pnext;
	pAB->pnext->pnext = m;
}


9.19写个小程序来测试一下这个链表容器



其实就是写1个c文件, 引用这个链表容器的头文件,调用我上面写的函数,就是可以测试了


代码如下啦:


int link_1(){
	//const char *n = "gateman poon";
	PERSON * p1 = person_new(1,"Jason Poon" );

	LINKPERSON * plink1 = link_create(10);
	//LINKPERSON * plink2;
	//link_traverse(plink2);

	//PERSON * p3;
	//printf("id is %d, name is %s\n",p3->id, p3->name);
	
	free(p1);
	p1 = link_getnode(plink1, 3);

	link_insert(plink1, link_getnode(plink1,4), person_new(24, "Gateman"));
	link_insertbyindex(plink1, 5, person_new(11, "Nedved"));
	link_remove(plink1, link_getnode(plink1,7));
	link_delete(plink1, 7);
	link_add(plink1, person_new(12, "Cindy"));
	link_add(plink1, person_new(24, "Gateman"));
	link_add(plink1, person_new(11, "Nvd11"));
	link_add(plink1, person_new(49, "Lulu"));
	link_add(plink1, person_new(47, "Alice"));
	link_traverse(plink1);

	printf("will be sort now!\n\n");
	link_sort(plink1);
	link_traverse(plink1);
	//link_clear(plink1);
	link_free(plink1);
	//link_traverse(plink1);

	//printf("id is %d, name is %s\n",p1->id, p1->name);
	printf("link1 done\n");
	return 0;

}

输出:



10,一些总结,

数组的优点:

          存取速度快, 因为根据头部指针就可以根据下标定位到对应的元素.

          缺点:

          实现必须知道数组的长度

          需要连续的内存空间

          插入和删除速度慢. 一旦插入或删除1个元素, 意味这大量元素要向前或向后移动1位.


链表的优点:

          空间几乎不用限制, 而数组必须要有连续的内存空间

          插入和删除元素的速度很快, 只需要修改部分指针.

链表缺点:

          存储速度慢. 因为要找1个元素就必须一直遍历下去..



    本文的函数就写到这里了,  其实还有很多功能没有实现, 例如求链表有效长度(这个太简单),   两个链表的对接(这个...也不难啦)等..  怎么说呢, 虽然伪算法不难看懂, 但是真正用代码来实现还是有点复9啊...

    关键就是对于指针的理解了, 对于数据结构, 指针机会就是一切啊....




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值