[C数据结构与算法]顺序表&链表

顺序表 Sequence List

顺序表的特点在于其将元素存放在一段连续的内存中,以便可以直接用下标访问. 因此顺序表主要由一段连续内存(可以是数组或者字符串)以及属性: 表长,表占用内存大小 组成.

顺序表因此可以被定义成:

typedef struct SeqList{
	int* base;	//Refer to a continuous memory storing elements
	int length;	//Refer to number of exsit elements
	int size;	//Refer to memory size allocate for base
}SList;

int* base : 一个指向int数组(连续内存)类型的指针

length : 标记表中已使用的单元内存,即顺序表元素个数

size : 标记表已占用的内存,顺序表的内存大小可以更改,在某些边界判断中通过使用realloc方法重新分配内存

  • 增删改查实现

    //Function for Sequence List
    bool initSL(SList* list){
    	list->base= (int*)malloc(sizeof(int)*DEFAULT_INIT_SIZE);
    	if(!list->base) return false;	//Memory overflow
    	
    	list->length= 0;
    	list->size= DEFAULT_INIT_SIZE;
    	
    	return true;
    }
    
    bool insSL(SList* list,int insPos,int insElm){
    	//Whether insPos(not index but the sequence) is out of range
    	if(insPos<1 || insPos>list->length+1) return false;
    	
    	//Check if enough size
    	if(list->length+1>list->size){
    		int* newbase= (int*)realloc(list->base,sizeof(int)*(list->size+DEFAULT_ENHANCE_SIZE));
    		
    		if(!newbase) return false;	//Memory overflow
    		list->base= newbase;
    	}
    	
    	//Shift elements after insPos backward
    	for(int idx=list->length;idx>=insPos;idx--)	
    		list->base[idx]= list->base[idx-1];
    		
    	list->base[insPos-1]= insElm;	
    	list->length++;	
    	
    	return true;
    }
    
    bool delSL(SList* list,int delPos){
    	//Whether delPos is out of range
    	if(delPos<1 || delPos>list->length) return false;
    	
    	//Shift elements after delPos frontwrad
    	for(int idx= delPos-1;idx<list->length-1;idx++)
    		list->base[idx]= list->base[idx+1];
    		
    	list->length--;
    	
    	return true;
    }
    
    bool modSL(SList* list,int modPos,int modElm){
    	//Whether modPos is out of range
    	if(modPos<1 || modPos>list->length) return false;
    	
    	list->base[modPos-1]= modElm;
    	
    	return true;
    }
    
    int findSL(SList* list,int elm){
    	//Check if a void list
    	if(list->length==0) return -1;	//-1 represent no result
    	
    	for(int idx=0;idx<list->length;idx++)
    		if(list->base[idx]==elm) return idx; //Return the index
    		
    	return -1;
    }
    
  • 总结

    数组,字符串同样可以被称为顺序表,它们可以使用下标访问,在内存上是连续的. 因此其时间复杂度低,但是顺序表不能够很好的利用细小的内存. 为了能够最大化利用内存,减小程序的空间复杂度,我们将介绍另一种线性存储结构,链表.

链表 Linked List

链表的特点在于其元素由一个一个节点组成(内存通常是非连续的),无法通过下标访问,必须从头遍历才能找到需要的那个节点. 链表通常由一个头指针,头节点(非必须),其他节点组成.

如下图所示:

链表结构图

头节点只是为了在实现链表的各种方法时更便捷,其结构与其他节点相同,都由数据域(指向节点存贮的数据,可以是int,char也可以是自定义的结构体等)与指针域(指向前或者后一个节点的指针,指针域拥有单向,双向指针的链表被称为单/双向链表)组成.

链表的节点因此可以定义成:

typedef struct LinkListNode{
	struct LinkListNode* next;	//Refer to next node
	int elm;	
	int no;
}LLNode;

struct LinkListNode* next : 指向下一个节点的结构体指针

int elm : 整数类型的属性

int no : 代表节点的序号,这不是构建链表所必需的,作者添加这个节点属性只是为了使编译结果更直接明白

在作者定义的这个节点中: *next 是指针域, 而int elm,int no 是节点存储的数据,即数据域

  • 增删改查实现

    //Function for Link List
    LLNode* initLL(){
    	LLNode* head= (LLNode*)malloc(sizeof(LLNode));
    	if(!head) return NULL;	//Memory overflow
    	
    	head->elm= 0;
    	head->no= 0;
    	head->next= NULL;
    	
    	return head;
    }
    
    bool insLL(LLNode* head,int insPos,int insElm){	
    	LLNode* ptr= head;
    	//Move ptr to the piror of insPos(not index but sequence)
    	for(int idx=1;idx<insPos;idx++)
    		ptr= ptr->next;
    		
    	//Create insNode
    	LLNode* insNode= (LLNode*)malloc(sizeof(LLNode));
    	if(!insNode) return false;
    
    	insNode->elm= insElm;
    	insNode->no= ptr->no+1;
    	//Connect insNode with next node
    	insNode->next= ptr->next;
    	//Cut off prior Node with next node
    	ptr->next= insNode;
    	
    	//Update the .No after insPos
    	while(ptr->next!=NULL){
    		ptr->next->no= ptr->no+1;
    		ptr= ptr->next;
    	}
    	
    	return true;
    }
    
    bool delLL(LLNode* head,int delPos){
    	LLNode* ptr= head;
    	
    	//Whether delPos is out of range
    	if(delPos<1) return false;
    	
    	//Move ptr to prior pos of delPos
    	for(int idx=1;idx<delPos;idx++){
    		ptr= ptr->next;
    		
    		if(ptr==NULL) return false;	//Out of range
    	}
    	
    	LLNode* delNode= ptr->next;	//Save the delNode
    	if(!delNode) return false;	//Memory overflow
    	ptr->next= ptr->next->next;	//Skip the delNode
    	delNode->next= NULL;	//Cut off delNode with next node
    	free(delNode);	//free the delNode which is why we save delNode
    	
    	//Update the .No after delPos
    	while(ptr->next!=NULL){
    		ptr->next->no= ptr->no+1;
    		ptr= ptr->next;
    	}
    	
    	return true;
    }
    
    bool modLL(LLNode* head,int modPos,int modElm){
    	LLNode* ptr= head;
    	
    	//Whether modPos is out of range
    	if(modPos<1) return false;
    	
    	//Move ptr to modPos
    	for(int idx=1;idx<=modPos;idx++){
    		ptr= ptr->next;
    		
    		if(ptr==NULL) return false;
    	}
    	
    	ptr->elm= modElm;
    	
    	return true;
    }
    
    int findLL(LLNode* head,int need){
    	LLNode* ptr= head->next;	//Skip head node
    	
    	//Check if a void list
    	if(ptr==NULL) return -1;	//-1 represent no result
    	
    	while(ptr!=NULL){
    		if(ptr->elm==need) return ptr->no; //return .No
    		
    		ptr= ptr->next;
    	}
    	
    	return -1;
    }
    
  • 例题

    1. 2. 两数相加 - 力扣(LeetCode)
      LLNode* solution(LLNode* listA,LLNode* listB){
      	//Create a new list for store answer
      	LLNode* listC= (LLNode*)malloc(sizeof(LLNode));
      	listC->next= NULL;
      	
      	//Tmp pointer
      	LLNode* ptrA= listA;
      	LLNode* ptrB= listB;
      	LLNode*	ptrC= listC;
      	
      	int carry= 0,A,B;
      	while(ptrA!=NULL || ptrB!=NULL){
      		//If ptr is NULL, which is two list has distinct length
      		if(ptrA==NULL) A=0;
      		else A= ptrA->elm;
      		if(ptrB==NULL) B=0;
      		else B= ptrB->elm;
      		
      		//Append a new node for list C
      		LLNode* newNode= (LLNode*)malloc(sizeof(LLNode*));
      		newNode->next= NULL;
      		newNode->elm= (A+B+carry)%10;
      		ptrC->next= newNode;
      		
      		carry= (A+B+carry)/10;
      		
      		ptrC= ptrC->next;
      		if(ptrA!=NULL) ptrA= ptrA->next;
      		if(ptrB!=NULL) ptrB= ptrB->next;
      		
      		printf("A: %d B: %d carry: %d\n",A,B,carry); 	//Debug
      	}
      	
      	//Do not forget the last carry
      	if(carry!=0){
      		LLNode* newNode= (LLNode*)malloc(sizeof(LLNode*));
      		newNode->next= NULL;
      		newNode->elm= carry;
      		ptrC->next= newNode;
      	}
      	
      	return listC;
      }
      
      • 思路: C= (A+B+carry)%10 其中 carry 来自上一位运算的进位. listC 中的每一位对应 listA,B 对应位之和+进位取模10.

      • 代码逻辑: 首先为三个链表创建临时指针,然后同时遍历链表AB中的每一位.

        在遍历中,考虑到链表长度可能不等,因此AB临时指针指向为空的节点的Elm将作为0,不影响链表C中位的计算.

        计算首位(即个位)时由于没有进位,carry声明为0,往后carry=(A+B+carry)/10

        创建,初始化,插入新节点到链表C,利用公式C= (A+B+carry)%10计算插入节点的elm

        每次遍历的最后记得移动临时指针,若已遍历到链表尾,则不移动,保持指针指向NULL,以便在下一次遍历时条件判断将该位作为0

        退出遍历后记得判断最后一位(即最高位)是否存在进位,这将导致链表C的长度比AB中最长链表的长度+1

    2. 约瑟夫环(循环链表结构)

      约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人

      如图 2 所示,假设此时圆周周围有 5 个人,要求从编号为 3 的人开始顺时针数数,数到 2 的那个人出列:约瑟夫环

      出列顺序依次为:

      编号为 3 的人开始数 1,然后 4 数 2,所以 4 先出列;

      4 出列后,从 5 开始数 1,1 数 2,所以 1 出列;

      1 出列后,从 2 开始数 1,3 数 2,所以 3 出列;

      3 出列后,从 5 开始数 1,2 数 2,所以 2 出列;

      最后只剩下 5 自己,所以 5 胜出。

      • 提示: 作者将使用单向的循环链表实现约瑟夫环,读者不必担心. 循环链表与普通链表区别仅仅在于末尾节点的next*指针, 对于普通链表,末节点的next*指针指向NULL,对于循环链表,末节点的next*指针指向头节点,仅此而已.
      #include <stdio.h>
      #include <windows.h>
      
      int totalPerson,startNum,roundNum;
      
      typedef struct LinkedList{
      	int num;
      	struct LinkedList* next;
      }Person;
      
      extern Person* initRoundList();
      extern Person* deletePerson(Person* beforeDelNode);
      
      
      void main(){
      	printf("Please input the total,start,round number\n");
      	scanf("%d %d %d",&totalPerson,&startNum,&roundNum);
      	
      	Person* head= initRoundList();
      	
      	//Move to the starter
      	Person* ptr= head;
      	for(int i=1;i<startNum;i++)
      		ptr= ptr->next;
      	printf("Let's start from %d\n",ptr->num);
      	
      	for(int i=1;i<totalPerson;i++){
      		//Move to the before delete one
      		for(int i=1;i<roundNum-1;i++)
      			ptr= ptr->next;
      			
      		printf("Kick number %d out of game\n",ptr->next->num);
      		head= deletePerson(ptr);	//Head will be next to the kicked person
      		ptr= ptr->next;	//Also ptr will be next to the kicked person
      	}
      	
      	printf("Number %d win the game\n",head->num);
      }
      
      Person* initRoundList(){
      	Person* head= NULL;
      	
      	Person* ptr=(Person*)malloc(sizeof(Person));
      	ptr->num= 1;
      	ptr->next= NULL;
      	
      	head= ptr;
      	
      	for(int i=2;i<totalPerson;i++){
      		Person* newNode= (Person*)malloc(sizeof(Person));
      		newNode->num= i;
      		newNode->next= NULL;
      		
      		ptr->next= newNode;
      		ptr= ptr->next;
      	}
      	
      	Person* endNode= (Person*)malloc(sizeof(Person));
      	endNode->num= totalPerson;
      	endNode->next= head;
      	
      	ptr->next= endNode;
      	
      	return head;
      }
      
      Person* deletePerson(Person* beforeDelNode){
      	Person* delNode= beforeDelNode->next;
      	
      	beforeDelNode->next= beforeDelNode->next->next;
      	delNode->next =NULL;
      	
      	free(delNode);
      	
      	return beforeDelNode->next;
      }
      
  • 总结

    在开发中,比起顺序表,链表是一种更常使用的数据结构,例如,在反恐精英:全球攻势这款游戏中,便使用双向链表存储人物结构体,代码形如下同:

    struct PlayerListNode{
    	struct PlayerListNode *next,*prior;	//Refer to next and prior list node
    	Player* player;	//Refer to the defined player struct
    	int playerNo;	//.No of player
    };
    

    这个人物链表节点的指针域是双向的,数据域除了玩家的人物编号外,还存在一个指向自定义的Player结构体指针用与储存玩家的各种信息如人物骨骼,手持物品,血量等

    这是因为链表能够利用碎片化内存的缘故. 在某些大型项目中,由于无法一次性,大量的分配内存,只能将数据分散到不同地址,分开存储. 由于链表的内存不是一次性分配的,每次插入节点时都会根据计算机内存占用的情况做出合理分配,因此链表对于降低程序空间复杂度具有重大的意义.


Reference

链表结构图来自C语言中文网 什么是单链表,链式存储结构详解 (biancheng.net)

约瑟夫环示意图来自C语言中文网 循环链表(约瑟夫环)的建立及C语言实现 (biancheng.net)

文章封面来自Pixiv画师ぢせ https://www.pixiv.net/artworks/70531218

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值