在我上一篇的博客中说过,数据结构的线性结构中有连续存储结构和离散存储结构,这次就写一写学习离散存储结构——链表的小总结。
首先说一下链表的定义:链表是物理存储单元上非连续、非顺序的存储结构,链表的结点间通过指针相连,每个结点只对应有一个前驱结点和一个后继结点,其中,首结点没有前驱结点,尾节点没有后继结点。上述表述共同构成了对链表的定义。
接下来说一下链表的结构:链表由头结点+有效节点构成,其中有效节点由分为首结点、普通节点、尾节点。
头结点:链表中有效结点前面的结点,头结点并不存放实际数据,仅仅是为了方便对链表的操作。
头指针:头结点并没有什么实际的含义,仅仅是为了方便对链表的操作,而头指针,顾名思义,就是指向头结点的指针。我们通过头指针指向的头结点的指针域对链表进行操作。
首结点:链表的第一个有效的结点。
尾结点:链表中最后一个有效的结点,尾结点的指针域为空。
链表已经学习完毕,那么接下来就是要说一下链表的构成——结点。因为链表是一张离散存储的结构,所以就需要通过地址去访问,那么久需要在结点中保存下一个结点的地址。还是用到c语言中的结构体。
1 //定义链表的节点 2 typedef struct Node{ 3 //数据域 4 int data; 5 //指针域 6 struct Node * pNext; 7 }NODE,*PNODE;
结点定义完毕后,就已经有了构成链表的元素,接下来就应该初始化链表。(算法具体解释请看代码注释)
1 /* 2 初始化创建链表的函数 3 初始化过程:因为链表由头结点+有效节点组成,所以首先应该定义一个头结点 4 */ 5 PNODE create_list(){ 6 int len; //链表中有效节点的个数 7 int val; //用来存放用户输入的临时数据的值 8 9 PNODE pHead=(PNODE)malloc(sizeof(NODE)); //定义一个PNODE类型的指针,并且令这个指针指向一个节点(头节点) 10 if(pHead==NULL){ 11 printf("分配失败,程序终止!"); 12 exit(-1); 13 } 14 PNODE pTail=pHead; //定义一个pTail指针,这个指针主要是用来指向尾节点(这个节点始终指向尾节点) 15 pTail->pNext=NULL; //因为始终指向尾结点,所以这个节点的指针域应该为空 16 17 printf("请输入所要创建链表的长度:"); 18 scanf("%d",&len); 19 20 int i; //定义循环变量 21 for(i=0;i<len;i++){ 22 printf("请输入第%d个结点的值:",i+1); 23 scanf("%d",&val); 24 25 PNODE pNew=(PNODE)malloc(sizeof(NODE)); //定义一个新的结点,这个结点用来表示新插入的结点 26 if(pNew==NULL){ 27 printf("分配失败,程序终止!"); 28 exit(-1); 29 } 30 31 /* 32 创建链表时,最主要的算法思想就是以下代码。 33 前面定义了一个pTail的指针,这个指针始终指向的是当前链表中的最后一个结点。 34 pTail->pNext=pNew; 所以,这段代码主要是将当前最后一个结点的指针域指向这个新结点,所以就将当前结点加入到了链表的最后面 35 pNew->data=val; 主要是将用户输入的值赋给新节点的数据域 36 pNew->pNext=NULL; 因为是最后一个结点,所以最后一个结点的指针域为空 37 pTail=pNew; 个人认为这段代码最核心的就在这里,在执行完上述两句代码时,其实已经将新的结点加入到链表的末尾,那么新的结点如何再加到链表的末尾? 38 就应该还向上面那样,令pTail指向最后一个结点,所以,pTail=pNew(这句话实际是将pNew的地址赋给pTail)。所以,当前pTail仍旧指向了链表最后的结点, 39 再来新的结点时,仍然会像上面那样执行。 40 */ 41 pTail->pNext=pNew; 42 pNew->data=val; 43 pNew->pNext=NULL; 44 pTail=pNew; 45 } 46 return pHead; //将链表的头结点返回 47 }
在创建一个链表后就应该能够遍历链表,其实遍历就根据链表的定义来就ok了,既然链表的每个结点都存储了下一个结点的地址,那么通过链表的地址去寻找结点就可以了。不多说,直接上代码
1 /* 2 链表遍历的方法 3 */ 4 void traverse_list(PNODE pHead){ 5 PNODE p=pHead->pNext; //定义一个指针p指向头结点的指针域,即p指向了链表的首结点 6 while(p!=NULL){ 7 printf("%d ",p->data); 8 p=p->pNext; //每次将p指向下一个结点 9 } 10 printf(" "); 11 }
接下来,写一下对链表排序的算法,这里只是采用最简单的算法(日后会专门学习排序算法),详细注释都在代码中。
1 /* 2 链表的排序方法 3 这里采用最简单的排序算法,从第一个元素一次和后面的元素进行比较,每次两两比较,最后将最小的值放在链表的最左边, 4 然后在从第二个元素向后比较,每次仍是两两比较,最小值放在链表第二个元素的位置,依次类推,直到链表只剩余了一个元素。 5 */ 6 void sort_linst(PNODE pHead){ 7 PNODE p,q; //定义两个PNODE变量,分别用于存储两个相互比较的元素 8 int val; //定义val变量,用于存储中间比较转换的值 9 int len=get_len(pHead); //定义len变量,用户存储链表的长度 10 int i,j; //定义i、j两个循环变量,因为当前通过指针无法标志当前为物理上的“第几个 ”元素,所以仍需单独定义循环变量 11 for(i=0,p=pHead->pNext;i<len-1;i++,p=p->pNext){ //当前为大范围的循环比较,最后一个不用比较,总体来说比较len-1次 12 for(j=i+1,q=p->pNext;j<len;j++,q=q->pNext){ //当前为小范围的循环比较,需要从头比较到尾,所以总体为len次 13 if(p->data>q->data){ 14 val=p->data; 15 p->data=q->data; 16 q->data=val; 17 } 18 } 19 } 20 }
当然,用到这种数据存储结构,必不可少的就要用到数据的插入和删除算法。
1 /* 2 链表插入算法 3 */ 4 int insert_link(PNODE pHead,int pos, int val){ 5 int i=0; //定义循环变量,用于指示链表在插入时,从第一个结点到被插入结点中的结点个数 6 PNODE p=pHead; //定义一个临时PNODE类型的p指针变量,并且令其指向头结点 7 while(p!=NULL&&i<pos-1){ //用于将p指向需要被插入的结点 8 p=p->pNext; 9 i++; 10 } 11 if(i>pos-1||p==NULL){ 12 return 1; //1表示当前插入不成功 13 } 14 15 //经过上述代码的过滤,当前p结点已经指向了需要被插入的结点,且pos是一个合理的插入位置 16 //插入算法实际就是令被插入位置的结点指向当前结点,然后当前结点指向原被插入的结点 17 PNODE pNew=(PNODE)malloc(sizeof(NODE)); 18 if(pNew==NULL){ 19 printf("内存分配失败"); 20 exit(-1); 21 } 22 pNew->data=val; 23 PNODE q=p->pNext; //用于记录原被插入结点的地址 24 p->pNext=pNew; 25 pNew->pNext=q; 26 return 0; //0表示当前插入成功 27 28 } 29 30 /* 31 链表的删除算法 32 */ 33 int delete_link(PNODE pHead,int pos ,int *val){ 34 int i=0; //定义循环变量,用于指示链表在插入时,从第一个结点到被插入结点中的结点个数 35 PNODE p=pHead; //定义一个临时PNODE类型的p指针变量,并且令其指向头结点 36 while(p!=NULL&&i<pos-1){ //用于将p指向需要被插入的结点 37 p=p->pNext; 38 i++; 39 } 40 if(i>pos-1||p==NULL){ 41 return 1; //1表示当前插入不成功 42 } 43 44 //删除算法的实质就是将当前被删除结点的前一个几点的指针域指向被删除结点的指针域所指向的地址 45 PNODE q=p->pNext; //令临时q指向被删除结点 46 *val=q->data; 47 p->pNext=p->pNext->pNext; 48 free(q); 49 q=NULL; 50 return 0; //0表示当前删除成功 51 }
在实验后,其实不太明白if(i>pos-1||p==NULL)这部分的控制,健壮性非常强,不明白为什么健壮性会这么强,如果有人看到希望能够帮忙解释一下,在这里先谢谢了。