文章目录
顺序表 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; }
-
例题
-
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
-
-
约瑟夫环(循环链表结构)
约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 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