链表
(以下内容均为单向链表)
链表是用链节指针链在一起的自引用结构变量(称为结点)的线性集合,是线性表的一种存储结构。假设我们想随心所欲创建一个结构,输入多少次数据他就有多大,可以说是游刃有余。相比数组和动态分配,这两者其实都有缺陷:数组,比如一个字符串数组,要存储书名,在我不知道所有名字到底多长时,字符串长度只能设置为最长的哪那个,浪费内存;动态分配内存,恐怕就是不断在malloc和realloc里,写起来难以理解不方便。所以介绍链表思想。
链表的性质:
(1)headPtr──指向链表首元结点的指针变量。(有的时候会有)
(2)每个结点由2个域组成:
数据域──存储结点本身的信息。
指针域──存储指向后继结点的指针(针对单向链表)。
(3)尾结点的指针域置为NULL(用反斜杠表示),作为链表结束的标志。
链表的特点:
1. 链表是一种存储结构,用于存放线性表;
2. 链表的结点是根据需要调用动态内存分配函数进行分配的,因此链表可随需要伸长缩短,在要存储的数据个数未知的情况下节省内存;
3. 链表的结点在逻辑上是连续的,但是各结点的内存通常是不连续的,因此不能立即被访问到,只能从首元结点开始逐结点访问。
如何访问链表中的结点:
由于链表中结点的内存是动态分配的,无法通过名称去访问,因此只能通过结点的地址去访问。
某结点的地址记录在其前驱结点的地址域里,因此要想访问第n个结点,必须先得访问第n-1个结点,读取该结点的地址域;而要想访问第n-1个结点,必须先得访问第n-2个结点;以此类推,一直推到访问第1个结点。而第1个结点是由指针headPtr指向的,因此能访问第1个结点,从而也就能访问第2个结点,第3个结点……从这一点上看,链表的访问效率比较低。
链表与数组特点对比:
数组的优点:
数组中的元素在内存中是连续存放的,能根据数组的首地址计算出各数组元素的内存地址,所以可以直接用下标访问到数组元素;而链表中的元素在内存中通常是不连续存放的,因此不能被立即访问到。
链表的优点:
1、可伸缩性:数组一旦在内存分配空间之后,大小就不能改变;而链表是动态的,在需要的时候可以增加或删减结点;数组的空间可能很快就用完,而链表只有在系统没有足够的内存满足动态分配存储空间的请求时 才会达到全满的状态;
2、插入和删除操作:数组的插入和删除涉及到移动元素的操作,因此比较费时;而链表的插入和删除比较简单;
对链表的基本操作:
创建链表、检索(查找)结点、插入、删除结点和修改结点等。
(1)创建链表是指:从无到有地建立起一个链表,即往空链表中依次插入若干结点,并保持结点之间的前驱和后继关系。
(2)检索操作是指:按给定的结点索引号或检索条件,查找某个结点。
(3)插入操作是指:在结点ki-1与ki之间插入一个新的结点k 。
(4)删除操作是指:删除结点ki,使线性表的长度减1。
下面我们依次看一下这些操作。
- 创建:
为了统一描述,先建立好结构及其代称方便之后描述:
typedef struct listnode list;
typedef list * listPtr;
struct listnode{
int data;
list *next;
};
【注】:创建的两种方法:
1.返回头指针值;listPtr create(){…}
2.执行对头指针地址值的改写;void create(listPtr *headPtr){…}
listPtr create() //创建并返回头指针
{
int num;
list *headPtr=NULL,*currentPtr=NULL,*lastPtr=NULL;
scanf("%d",&num); //以输入数据为-1结束
while(num!=-1){
currentPtr=(listPtr)malloc(sizeof(list)); //给current分配新的空间,使其进一步变为一个存储信息的节点
if(currentPtr!=NULL)
currentPtr->data=num;
if(headPtr==NULL) //如果头指针为空,那么把current给他,并用lastPtr记录current的当前位置
{
headPtr=currentPtr;
lastPtr=currentPtr;
}
else{ //否则,把上一个lastPtr的next指针和当前的节点连起来,然后继续用lastPtr记录current位置
lastPtr->next=currentPtr;
lastPtr=currentPtr;
}
scanf("%d",&num);
}
lastPtr->next=NULL; //一定记得把链表尾节点的next指向空NULL
return headPtr;
}
如果实在无法理解创建过程,可以这样想:比如lastPtr这个辅助指针,
lastPtr->nextPtr=currentPtr;
lastPtr=currentPtr;
这两句话其实是说:我之前构造的空间,它的表尾与下一个新空间进行了联通,那么现在就实现了链接,我需要让lastPtr继续服务,就必须让它继续充当“最后一个空间的牌面”,因而把新空间的地址交付给他,这一切完成的只是“利用名字找对地址,进而使地址名下的空间实现连通”的过程而已,一个节点的地址可以用多个名字表示的。
- 打印:
顺序遍历,不为空就打印出来
void print(listPtr headPtr) //打印
{
listPtr current=NULL;
if(headPtr==NULL)
printf("None\n");
else{
current=(listPtr)malloc(sizeof(list));
current=headPtr;
while(current!=NULL){
printf("%d\n",current->data);
current=current->next;
}
}
}
- 释放:
创建时malloc申请地空间的常规操作
使用辅助节点current记录地址位置以防头结点信息丢失
void destroy(listPtr headPtr){
list *current=NULL;
while(headPtr!=NULL){
current=headPtr;
headPtr=headPtr->next;
free(current);
}
}
- 插入:
基本思路就是,从头开始往后遍历,设置current,用number进行遍历检索时,同时记录current前面一个节点prePtr,当number到达适合的位置时,就在prePtr和current节点之间插入新节点(当然,前提是这个链表本来就是升序的)
listPtr insert(listPtr headPtr) //插入
{
int num;
list *prePtr=NULL,*newPtr=NULL,*current;
scanf("%d",&num);
newPtr=(listPtr)malloc(sizeof(list));
newPtr->data=num;
current=headPtr;
while(current!=NULL&&num>current->data){
prePtr=current;
current=current->next;
}
if(prePtr==NULL){
newPtr->next=current;
headPtr=newPtr;
}
else if(current==NULL){
current->next=newPtr;
newPtr->next=NULL;
}
else{
prePtr->next=newPtr;
newPtr->next=current;
}
return headPtr;
}
- 删除:
就是遍历,找适合条件的节点,然后把其前节点prePtr和其本身后节点next连起来
listPtr delete(listPtr headPtr) //删除
{
int num;
scanf("%d",&num);
list *prePtr=NULL,*current;
current=headPtr;
while(current!=NULL&&num!=current->data){
prePtr=current;
current=current->next;
}
if(prePtr==NULL){
headPtr=headPtr->next;
}
else if(current==NULL){
printf("You ass fool,there's no such kind of node.\n");
}
else{
prePtr->next=current->next;
}
return headPtr;
}
链表的应用:
- 对链表内排序:升序链表的创建
其实道理很简单,在创建过程中,没开辟一个新空间节点current,就把current插入到原链表中去,就实现了升序创建。
listPtr paixu(){
int num;
scanf("%d",&num);
list *headPtr=NULL;
while(num!=-1){
headPtr=insert(headPtr,num);
scanf("%d",&num);
}
return headPtr;
}
listPtr insert(listPtr headPtr,int num){
int b=0;
list *prePtr=NULL,*current,*newPtr=NULL;
newPtr=(listPtr)malloc(sizeof(list));
newPtr->data=num;
if(headPtr==NULL){
headPtr=newPtr;
headPtr->next=NULL; //否则程序在第二次进行插入时会卡死
}
else{
current=headPtr;
while(current!=NULL&&num>current->data){/*current!=NULL这句话要先写,
C语言只要前者不行就不判断后者,这样,当current是NULL时不会造成
“无法访问任何东西”,这被称为“逻辑短路”,不要越界访问!*/
prePtr=current;
current=current->next;
}
if(prePtr==NULL){
newPtr->next=current;
headPtr=newPtr;
}
else if(current==NULL){
prePtr->next=newPtr;
newPtr->next=NULL;
}
else{
prePtr->next=newPtr;
newPtr->next=current;
}
}
return headPtr;
}
我们也可以,冒泡排序,基本思路就是,记录要被交换的节点A的prePtr和节点B的prePtr和节点B自己的位置,三节点交换。这里先用辅助量count(链表元素个数)方便操作
listPtr order(listPtr headPtr,int count){
int i,j;
list *prePtr1=NULL,*prePtr2=NULL,*current=NULL;
if(count==1)
return headPtr; //直接判断特殊情况,节省时间先
else{
for(i=count;i>0;i--){
prePtr1=headPtr;
prePtr2=prePtr1->next;
current=prePtr2->next;
for(j=0;j<i&&prePtr2->next!=NULL;j++){
if(j==0&&prePtr1->data > prePtr2->data){ //如果判断的是第一、第二个节点且要交换
prePtr1->next=prePtr2->next; //三节点交换法,不懂可以画箭头图分析
prePtr2->next=prePtr1;
headPtr=prePtr2;
prePtr1=headPtr;
prePtr2=prePtr1->next;
current=prePtr2->next; //保持三个节点相对顺序,这样可以一直判断下去
}
if(j>0&&prePtr2->data > current->data){
prePtr1->next=current;
prePtr2->next=current->next;
current->next=prePtr2;
}
if(j>0){
prePtr1=prePtr1->next;
prePtr2=prePtr1->next;
current=prePtr2->next;
}
}
}
}
return headPtr;
}
- 链表节点段的交换问题:
上文只涉及单节点交换,接下来我们完成从[s1],[t1]到[s2],[t2]的片段交换。
坑:1.如果s1=t1等怎么处理?我的算法由于记录了整8个节点所以相对安全
2.如果t1=s2-1?s1=0?{
情况一:t1=s2-1如图过程:
情况二:t1!=s2-1如图:
listPtr exchange(listPtr headPtr,int s1,int t1,int s2,int t2)
{
list *prePtr1=NULL,*prePtr2=NULL,*leftPtr;
list *preTemp1=NULL,*lastTemp1,*preTemp2=NULL,*lastTemp2;
prePtr1 =find(headPtr,s1-1); preTemp1 =find(headPtr,s1);
lastTemp1=find(headPtr,t1);
prePtr2 =find(headPtr,s2-1); preTemp2 =find(headPtr,s2);
lastTemp2=find(headPtr,t2);
if(s2!=t1+1){
leftPtr=lastTemp1->next;
lastTemp1->next=lastTemp2->next;
if(s1>1)
prePtr1->next=preTemp2;
lastTemp2->next=leftPtr;
prePtr2->next=preTemp1;
}
else{
if(s1>1)
prePtr1->next=preTemp2;
lastTemp1->next=lastTemp2->next;
lastTemp2->next=preTemp1;
}
if(s1!=1)
return headPtr;
else
return preTemp2;
}
listPtr find(listPtr headPtr,int where) //找到第where个节点并返回其地址位置
{
int count=0;
list *current=NULL,*prePtr=NULL;
current=headPtr;
if(where==0)prePtr=headPtr;
while(count<where&¤t!=NULL){
prePtr=current;
current=current->next;
count++;
}
return prePtr;
}
以下为双向节点链表(虽然不完全是):值得注意的是,对某些节点定义好内部pre指针的方向,将有助于大大简化操作
typedef struct node{
int value;
struct node *next;
struct node *pre;
}Node; //以及:不再需要find函数!
Node* exchange(Node *head,int s1,int t1,int s2,int t2){
Node *ps1,*pt1,*ps2,*pt2;
Node *p = head->next;
for(int i = 1;p;p = p->next,i++){
if(i==s1) ps1 = p;
if(i==t1) pt1 = p;
if(i==s2) ps2 = p;
if(i==t2) pt2 = p;
}
Node *temp1 = ps1,*temp2 = ps2;
ps1->pre->next = temp2;
temp2->pre->next = temp1;
Node *temp3 = pt1->next,*temp4 = pt2->next;
pt2->next = temp3;
pt1->next = temp4; // 三变量法交换
return head;
}
其余略......
- 逆序输出问题:
方法一:在创建的时候,每次制造完新节点,都让他接到head前面,再把head调到首位,循环此操作。
listPtr create(){
list *headPtr,*current;
int num;
scanf("%d",&num);
while(num!=-1){
current=(listPtr)malloc(sizeof(list));
current->data=num;
if(headPtr==NULL){
headPtr=current;
headPtr->next=NULL;
}
else{
current->next=headPtr;
headPtr=current;
}
scanf("%d",&num);
}
return headPtr;
}
方法二:拆解,重构。从头开始,依次拆解一个节点的next指向,并指向新节点:其previous one。
listPtr reverse(listPtr headPtr){
list *prePtr,*current,*frontPtr;
if(headPtr==NULL)
return NULL;
else{
prePtr=headPtr;
current=prePtr->next;
headPtr->next=NULL; //注意!因为此时head的位置是作为最终链表的尾节点
while(current!=NULL){
frontPtr=current->next;
current->next=prePtr;
prePtr=current;
current=frontPtr;
}
}
return prePtr; //别传错了,head此时只是尾节点
}
方法三:递归,不到最后一项不打印。先函数,后打印
void print(listPtr headPtr){
list *current;
current=headPtr;
if(current!=NULL){
print(current->next);
printf("%d",headPtr->data);
}
}
- 链表归并问题:
思路:明确我想把head2插到head1里面去,实现过程:首先,针对head2的首数,看一看它在head1里能找到哪个夹缝塞进去,即对head1遍历,head2->data为标尺确定prePtr1和current1 然后看看从head2开始能有几个节点能进入夹缝,即对head2遍历,current1->data为标尺卡出prePtr2和current2,确定后,插入后,把head2换到current2位置,继续搜索插入。
有关leftPtr:这是一个针对head的辅助变量,因为我们发现两个head都有可能被改写,而head2的改写可能是“丢失性的”,leftPtr能帮助我们控制好head2的地址得以保留。
listPtr merge(listPtr headPtr1,listPtr headPtr2){
list *prePtr1,*prePtr2,*current1,*current2,*leftPtr;
while(headPtr2!=NULL){
leftPtr=headPtr2;
prePtr1=NULL;
prePtr2=NULL;
current1=NULL;
current2=NULL;
find(headPtr1,leftPtr->data,&prePtr1,¤t1);
if(current1==NULL){
prePtr1->next=leftPtr;
headPtr2=NULL;
}
else{
find(headPtr2,current1->data,&prePtr2,¤t2);
if(current1==headPtr1){
prePtr2->next=headPtr1;
headPtr1=leftPtr;
}
else{
prePtr1->next=leftPtr;
prePtr2->next=current1;
headPtr2=current2;
}
}
}
return headPtr1;
}
void find(listPtr headPtr,int num,listPtr *prePtr,listPtr *current){ //相互查询找位子
list *currentTemp,*preTemp;
currentTemp=headPtr;
preTemp=NULL;
while(currentTemp!=NULL&¤tTemp->data<=num){
preTemp=currentTemp;
currentTemp=currentTemp->next;
}
*prePtr=preTemp;
*current=currentTemp;
}
- 约瑟夫环问题:
在一个“循环链表”里(即lastPtr->next=headPtr),每次让prePtr处于“零位”,那么在每次循环开始,我让current指针开始向后移位,并结合count递增,就能模拟“从一开始依次报数”了
首先,第一次循环current,使current到达尾节点。第二次循环,初,用prePtr记录尾节点“零位”,current后移到达1,报数开始,如果可除名,除,利用prePtr,实现新“零位”。
比如猴子选大王
listPtr create(int n){ //创建有n个节点的链表
list *headPtr=NULL,*current=NULL,*lastPtr=NULL;
int i;
for(i=1;i<=n;i++){
current=(listPtr)malloc(sizeof(list));
current->data=i;
if(headPtr==NULL){
headPtr=current;
lastPtr=current;
headPtr->next=NULL;
}
else{
lastPtr->next=current;
lastPtr=current;
}
}
lastPtr->next=headPtr;
return headPtr;
}
listPtr choose(listPtr headPtr,int n,int m){
list *prePtr,*current,*lastPtr;
int count=0;
current=headPtr;
while(current->next!=headPtr)
current=current->next; //找到最后一个区
while(current->next!=current){
prePtr=current;
current=current->next;
count++;
if(count%m==0){
prePtr->next=current->next;
printf("It's '%d!\n",current->data);
free(current);
current=prePtr;
}
}
printf("Our king:%d!!!\n",current->data);
free(current);
}
//因为是循环链表,除非告知n,要不释放起来比较麻烦,而边创建新链接边释放就方便得多,也符合“出局”要求
总之,链表的思想精髓在于 交换位置 而非 交换内容(当然交换内容也行,但锻炼不了思维方式),在某些对数据结构要求高的方面,链表是极其基础而重要的工具。