双向循环链表

 下面将以我之前做的小项目(ATM存取款系统)的部分代码来为大家介绍有点另类的双向循环链表。

结构体:

#ifndef __LIST_H___
#define __LIST_H___

#include 
#include 
#include 
#include 
#include 
#include 
#include 

typedef struct 
{
	char name[20];//名字
	char sex[10];//性别
	char card[20];//账号
	int  age;//年龄
	char phone[20];//电话
	char bankcard[20];//银行卡号
	char password[10];//密码
   	float balance;//余额
}student_t;

typedef struct node{
	void *datap;//指向数据的指针
	struct node *prev,*next;
}DNode_t;

//抽象链表线性表信息头
typedef struct{
	int n;		//数据的个数,可以说是链表的节点数(不包括头节点)
	int size;	//数据空间大小	
	
	DNode_t head;	//链表头
}LIST_T;

 可能一时间看不懂为什么这样写,这样写有什么好处呢?

typedef struct node{
	char name[20];
	char sex[10];
	char card[20];
 	int  age;
 	char phone[20];
	char bankcard[20];  
 	char password[10];
    	float balance;
    	struct node *prev,*next;
}LIST_T;

 大部分人初学者写链表结构体是上面这样写的,把链表跟要插入链表的详细数据弄在一起操作,但是有没有想过这样写,会把它给写死了,就是如果我突然不想插入下面的信息到链表里面,现在只需要把一个整型数插入到链表里面,那是不是要把整个链表给改了才行。改成下面这样:

typedef struct node{
	int num;
	struct node *prev,*next;
}LIST_T;

 所有说,换种数据插入到链表里,又要把整个链表结构体给改了,那为什么不把链表结构体写的通用性一些呢。现在应该知道的链表结构体为什么是下面这样了吧。

typedef struct node{
	void   *datap;//指向数据的指针
	struct node *prev,*next;
	

 为什么说这样写比较通用呢?
首先void *datap,它是一个万能指针,可以存放任意类型变量的地址,给它分配要存储的数据大小内存,把要存储的数据拷贝到内存里就行了,也就是它指向了你存储数据的地址,而且存储什么类型都可以,无需大改,只需要操作数据大小、数据赋值和拷贝数据我这里的数据就是一开始定义的student_t结构体,你们自己定义,把它单独弄出来,上面这个结构体不需要改动,后面代码会陆续讲到。

注意:万能指针不能直接解引用,要强制转化为存储数据的类型,在进行操作。


我们看下面的代码

//抽象链表线性表信息头
typedef struct{
	int     n;    //数据的个数
	int     size; //数据空间大小
	DNode_t  head;//链表头
}

 这个又为什么这样写呢?首先我们要知道数据空间的大小才能分配内存,数据赋值是用户做的事,我们只管分配内存、拷贝数据到内存、插入链表里,所以我们要数据空间的大小,如果把它弄到上一个结构体里面,封装性就不好了,那样每次都要改结构体,还把数据空间大小插入链表里头,而且每次都要赋值。所以我们可以定义一个结构体,把链表抽象出来,弄到一个结构体里面,就是把链表和描述链表的信息放在一个结构体里面,初始化一次结构体就行了,这样方便好多,还添加了一个数据个数,代表着有多少节点(不包括头节点),这样就可以马上知道有多少人注册了账号等等,不需要遍历来求出个数,是不是很方便?


 下面将以插入信息和遍历,来为大家解释,因为代码量有点大不好解释,所以只讲一小部分代码,请大家见谅!!!。

list.h

typedef struct 
{
     char name[20];
     char sex[10];
     char card[20];
     int  age;
     char phone[20];
     char bankcard[20];  
     char password[10];
     float balance;
}student_t;

typedef struct node{
	void   *datap;//指向数据的指针
	struct node  *prev,*next;
}DNode_t;

//抽象链表线性表信息头
typedef struct{	
	int     n;    //数据的个数
	int     size; //数据空间大小
	DNode_t  head;//链表头
}LIST_T;
	
//学生信息
int initStu(student_t *s);

//初始化空的链表线性表
LIST_T  *initList(int  size);

//头部追加数据
int  appendListHead(LIST_T  *ptr,const void *datap);

//遍历
void travelListNext(LIST_T  *ptr,void (*show)(void *datap));

//总数
int  lengthList(LIST_T *ptr);

//回收资源
void destoryList(LIST_T *ptr);

//打印某个节点的信息
void showStu(void *datap)
//打印整数
void shownum(void *datap)

list.c

#include"list.h"
//初始化空的链表线性表
LIST_T  *initList(int  size)//size数据空间的大小,你要根据你要存储数据大小来定义
{
     LIST_T  *tmp;
     tmp=malloc(sizeof(LIST_T));//分配内存空间
     if(NULL==tmp)   return NULL;

     tmp->n   =0;//节点个数(不包括头节点)
     tmp->size=size;//数据空间大小 			
     //头节点 自己指向自己
     tmp->head.next=&tmp->head;
     tmp->head.prev=&tmp->head;
     return tmp;
 }
//单纯插入  把插入弄出来,那样头部插入、尾部插入、有序插入都可以用代码更加的简洁明了
static void __insertEntry(DNode_t  *new,DNode_t  *P,DNode_t  *N)
{   
     new->next=N;
     new->prev=P;
     N->prev=new;
     P->next=new;
}

//头部插入
int  appendListHead(LIST_T  *ptr,const void *datap)
{
     int ret;
     DNode_t  *one;//定义新节点
     //分配数据节点空间  为链表结构体分配空间
     one=malloc(sizeof(DNode_t));
     if(one==NULL)   return -1;
     //分配数据空间 为你要存储的数据数据分配空间并让万能指针void *datap指向
     one->datap=malloc(ptr->size);
     if(one->datap==NULL)
     {
         free(one);
         return -2;
     }
     //数据复制   
     memcpy(one->datap,datap,ptr->size);

	 //将该数据节点插入到链表的头节点后
     __insertEntry(one,&ptr->head,ptr->head.next);

	//数据个数加1  
     ptr->n=ptr->n+1;
     return 0;
}

///遍历///
void travelListNext(LIST_T  *ptr,void (*show)(void *datap))
{
     DNode_t  *t;
     t=ptr->head.next;
     while(t!=&ptr->head)
     {
        show(t->datap);
        t=t->next;
     }
}

//注册账号
int initStu(student_t *s)
{
    int ret,i;
    char buf[10]={0};
    printf("请输入相关信息\n");
    printf("请输入姓名:");
    scanf("%s",s->name);
    
    printf("请输入性别:");
    scanf("%s",s->sex);
    
    printf("请输入身份证卡号:");
    scanf("%s",s->card);

    printf("请输入年龄:");
    scanf("%d",&s->age);

    printf("请输入电话号码:");
    scanf("%s",s->phone);

    printf("注册成功\n");
    
    //随机产生19位的银行卡号
    memset(s->bankcard,0,sizeof(s->bankcard));
    for(int i=0;i<19;i++)
        *(s->bankcard+i)=rand()%10+'0';
    printf("你的卡号为:");
    printf("%s\n",s->bankcard);
    system("stty -icanon");//关闭缓存区
    system("stty -echo");//关闭回显 就是键盘输入不显示出来 实现输入密码是*
    while(getchar()!='\n');
    do{
        printf("设置6位登陆密码:");
    for(i=0;i<6;i++)
    {
        scanf("%c",&s->password[i]);
        printf("*");
    }
    s->password[6]='\0';
    ret=strlen(s->password);
    if(ret!=6)
    {
        printf("设置失败,请重新设置!!!\n");
        continue;
    }
    }while(ret!=6);
	
	while(1)
    {   
        do{
            printf("再次输入登陆密码:");       
            for(i=0;i<6;i++)
            {
                scanf("%c",buf+i);
                printf("*");
            }
            buf[6]='\0';
            ret=strlen(buf);
            if(ret!=6)
            {
                printf("输入失败,请输入设置!!!\n");
                continue;
            }
		  }while(ret!=6);
            int ret=strcmp(s->password,buf);
            if (ret==0)
            break;
            printf("密码不一致:\n");
	   }
	    system("stty icanon");//打开缓存
	    system("stty echo");//打开回现
	    printf("恭喜你!!!,完成注册手续。\n");
	    s->balance=0;
 	    printf("\n");
           return 0;
}
//打印某个节点的信息
void showStu(void *datap)
{
 student_t *s=(student_t *)(datap);
 printf("姓名:%s 性别:%s 身份证号:%s 年龄:%d 电话号码:%s 银行卡号:%s 登陆密码:%s 余额:%f\n",
 s->name,s->sex,s->card,s->age,s->phone,s->bankcard,s->password,s->balance);
}

void shownum(void *datap)
{
	int *num=(int *)(datap);
	printf("num:%d\n",*num);//打印一个整型数
}
//数据个数
int  lengthList(LIST_T *ptr)
{
    return ptr->n;
}
//回收资源
void destoryList(LIST_T *ptr)
{
     DNode_t  *t,*next;
     t=ptr->head.next;
     while(t!=&ptr->head)
     {
        next=t->next;
        free(t);
        t=next;
     }
     free(ptr);
}

main.c

//单纯的插入、遍历
#include "list.h"
int main(int agrc,char **agrv)
{
	system("clear");//清屏
	int k,ret;
	//可以定义两条链表 要看你自己的需求  一条结构体数据,一条整型数  下面简述只能一条要么结构体,要么整数
	LIST_T *intp;//抽象链表线性表信息头
	//LIST_T *ptr;//抽象链表线性表信息头
	student_t stu;//定义数据结构体 

///	 
#if 1 //插入数据结构体信息到链表
	intp = initList(sizeof(student_t));//初始化抽象链表线性表信息头	参数是你要存储数据的大小  这里是list.h里的结构体
#elif 0
	intp = initList(sizeof(int));//初始化抽象链表线性表信息头 参数是你要存储数据的大小  这里是一个整型
#endif 	

	if(intp==NULL)
	{
	  fprintf(stderr,"init list failed.\n");
	  return 1;
	}
	while(1)
	{
	
#if 1   //插入数据结构体信息到链表
		if(initStu(&stu)==-1) break;//对数据结构体赋值
		ret=appendListHead(intp,&stu);//插入到链表里头 就是将数据拷贝到所void *datap所分配的数据空间并插入链表  头部插入
		
#elif 0 //插入一个整型数到链表
 		int num;
  		if((ret=scanf("%d",&num))!=1) break;//对整型数据赋值
  		ret=appendListHead(intp,&num);//插入到链表里头 就是将数据拷贝到所void *datap所分配的数据空间并插入链表  头部插入
#endif  

		if(ret!=0)
            	{
               		fprintf(stderr,"append data to  list failed.\n");
               		break;
           	}
           	k=lengthList(intp);//直接获取结构体intp里n(数据个数)就行了,不用遍历来查看个数
    		printf("卡号个数:%d\n",k);//卡号个数
    			
    		///遍历
#if 1    		
		//结构体
           	travelListNext(intp,showStu);//查询全部信息,也就是全部节点
#elif 0         
           	//整型数
           	travelListNext(intp,shownum);//查询全部信息,也就是全部节点
#endif           	
	 }
	 //回收资源
  	 destoryList(intp);
 }
 

 上面一个插入.h里的数据结构体到链表,一个是插入整型数到链表,只要重新初始化抽象链表线性表信息头和改变数据操作就行,例如:遍历,遍历函数不用变,只需变第二个参数(对应数据的显示函数),初始化抽象链表线性表信息头时改数据空间大小等。

 可以有些人对遍历函数void travelListNext(LIST_T *ptr,void (*show)(void *datap));不太理解,下面为大家解释一下:

void travelListNext(LIST_T  *ptr,void (*show)(void *datap))
{
     DNode_t  *t;
     t=ptr->head.next;
     while(t!=&ptr->head)
     {
        show(t->datap);//参数是:指向数据的万能指针void *datap   
        t=t->next;
     }
}

//打印某个节点的信息
void showStu(void *datap)
{
 student_t *s=(student_t *)(datap);//因为是万能指针不能直接操作,要强制转换成存储的数据类型
 printf("姓名:%s 性别:%s 身份证号:%s 年龄:%d 电话号码:%s 银行卡号:%s 登陆密码:%s 余额:%f\n", s->name,s->sex,s->card,s->age,s->phone,s->bankcard,s->password,s->balance);
}

 首先,定一个参数是你的抽象链表线性表信息头,第二个是函数指针:存储函数地址的指针变量,跟他的返回值类型和参数类型都没关系,存放你要显示数据的函数,你要怎么显示你的数据只要改你的显示函数就行,想怎么显示就怎么显示。例如:travelListNext(intp,showStu);

上面大部分函数的参数void *类型的,知道为什么吗?
你想想如果你弄成了int *,那么是不是只能传int *类型过去,想传其他类型是不是要强制转换,比如你要传类字符串过去,你是不是要对字符串强制转换成int *。
所以要写成void *,因为这样写通用性很强,void *存放任何类型地址。你们要记住封装函数时,不要写死了,想想怎么写,别人也能用到你的函数。

 在这里我在提一点:我们插入到链表或者查询链表有没有那个数据等等,是不是每次调用函数后要判断成不成功,如果多处地方调用是不是都要写判断是否成功,弄得代码不够简洁,我们可以把所函数调用跟判断再弄成一个函数,那样直接调用函数就行了,多处地方调用也只需一个函数,是不是简洁很多。你们可以试一试。

 最后我布置一道练习,编写一个查找函数,就是查找链表有没有匹配的数据,有就返回数据空间地址(如果多个数据匹配只返回第一个),没有就返回NULL,大家试写一下。
提示:
/*
*compare_t 比较函数类型
*d1 数据大于 d2 返回大于0 的值
*d1 数据等于 d2 返回等于0 的值
*d1 数据小于 d2 返回小于0 的值
*/
typedef int compare_t(const void *d1,const void *d2);
//compFunc比较函数指针 第一个参数为链表中的数据指针 第二参数为key查找关键字
void *searchOneListByCond(LIST_T *ptr,compare_t *compFunc,const void *key);


例子:
typedef int compare_t(const void *d1,const void *d2) ;
compare_t fun;//其实就是int fun(const void *d1,const void *d2);
.
typedef void func_t(char *)
func_t sayHello; //void sayHello(char *name);
.
typedef int compare_t(const void *d1,const void *d2) ;
所以compare_t *compFunc其实就是定义了存放参数是(const void *d1,const void *d2),返回值是int 类型的函数地址的函数指针。这个函数用来比较的,如果数据匹配返回对应的值。
提示就到这里啦,大家有空可以写写。

全篇完。

本人博客仅仅代表我个人见解方便记录成长笔记。

若有与 看官老爷见解有冲突,我坚信看官老爷见解是对的,我的是错的。

感谢~!

你可能感兴趣的:(数据结构)