线性数据结构
一、总论
线性数据结构是极适用于模拟算法的一种数据结构。这么说好像不太精确,一方面,确实链表,循环链表,数组,栈,队,都可以很精确的模拟某种背景,另一方面,线性数据结构,尤其是栈,队,链表,又代表着某种抽象的数学思想,从而可以被应用到那些可能不是那么明显的题目中,比如递归,BFS,DFS,哈希中。在进一步讲,任何一种数据结构或许都兼具模拟和抽象两方面的应用,只是因为我现在刚学完线性表,所以对他感触较深罢了。
正是因为如此,我现在学习的时候往往陷入误区:有的数据结构过于擅于模拟,所以我就不会思考用其他数据结构来对这道题进行模拟,比如约瑟夫问题只会用循环链表来写,但忽略了可以用队列的方法进行处理;有的数据结构擅于抽象,但是真的落实到代码上,过于抽象的思想,往往有细节上面的问题,如双向链表。
谈及数据结构,切不能以为这是一些固定的、可以稍微改动一下就能使的代码块。结构就是关系,每种数据结构大多只强调其中的一种关系,比如数组强调随机存储、链表强调前后关系、栈强调先进后出,但是如果想更快更好的解决问题,有两个方向。一个是改造数据结构,使其在保证原有结构思想的前提下功能更强大,比如辅助栈,另一个是将不同的结构思想进行合并,比如用数组实现链表和树,就兼具了相互关系和随机存取的优点。
二、约瑟夫问题
2.1 用队列解决约瑟夫问题
for(i=1; i<=n; i++)
EnQueue(i);
while(IfEmpty() == 0)
{
if(cnt == m)
{
printf("%d ",DeQueue());
cnt=1;
}
else
{
EnQueue(DeQueue());
cnt++;
}
}
可以看到,用队列的方法可以很容易的解决这个问题,但是我偏偏不敢想,导致每次都做题都及其繁琐。
如果在进一步讲,约瑟夫问题可以抽象为循环、删除、前后联系这几个特征,只要满足这几个特征的数据结构都可以解题,比如懒惰删除的数组,只要控制好循环变量,就可以使其具有前后联系的特征,再结合到一定值归零,就可以使其具有循环的特征,这样这个题目就可以用数组解了。
2.2 头结点的某一意义
头结点最显然的意义就是可以维持各种操作的形式不变性,每次不需要对头部进行分类讨论。但是如果深究的话,其实对于一个非循环单链表,我们肯定不希望它的数据丢失,那么必须时时刻刻有一个指针指向第一个元素,不然数据就丢到游标指向的当前元素了,之前的元素因为没办法回溯,所以就找不到了,所以无论是头结点还是头指针,都在解决单链表自身的局限性,而并非什么优化手段。
换而言之,只要链表不再有前面丢失属性,那么头节点的存在意义就大打折扣了,比如约瑟夫问题,对头结点的分类讨论会让人抓狂,因为要维持循化不变性,在经过头结点时,要讨论两遍要不要再移动一次。
2.3 无头节点循环单链表
pre=InitCyc(n);
cnt=1;
while(n)
{
if(cnt == m)
{
printf("%d ",pre->next->value);
Delete(pre);
cnt=1;
n--;
}
else
{
cnt++;
pre=pre->next;
}
}
node* InitCyc(int n)
{
int i;
node *cur=NULL, *head=NULL;
for(i=1; i<=n; i++)
{
node* tmp=(node*)malloc(sizeof(node));
tmp->value=i;
tmp->next=NULL;
//因为没有头结点而付出的代价
if(head == NULL)
head=tmp;
else
cur->next=tmp;
cur=tmp;
}
cur->next=head;
//没用双向链表的代价
node* pre=cur;
return pre;
}
void Delete(node* pre)
{
node* cur=pre->next;
pre->next=cur->next;
free(cur);
}
这是一个没有头结点的循环单链表,可以看到代码明显要比有头结点的要简洁很多,除了有一个head来保存初始值,方便组建循环链表以外,基本上没有需要分类讨论的地方。这道题的最完美解法是无头节点的双向循环链表,但是对于双向链表的讨论我想要放到下一节讨论。
三、P1160 队列安排
3.1 双向链表在删除操作中的优势
用单向链表很麻烦,是因为删除操作要的是pre指针,而删除哪个元素却要看cur指针,但是第一个要删除元素的pre指针理论上很难获得,除非我遍历循环链表一整遍(如果不是循环链表的无头节点链表,基本上没法获得pre)我在这里用的是在创建的时候返回了第一个节点前驱的指针,但是就会使形式很难看,但是如果用双向链表删除可以只利用cur指针,简单易懂 。
在这里补录删除操作和插入操作,都具有很好看的代码。
//这个删完以后cur会指向下一个,用于约瑟夫问题
node* Delete(node* cur)
{
node* tmp=cur;
cur=cur->front;
cur->next=cur->next->next;
cur=cur->next;
cur->front=cur->front->front;
free(tmp);
return cur;
}
//这个删完以后就是删完了
void Delete(node* cur)
{
cur->next->prior=cur->prior;
cur->prior->next=cur->next;
free(cur);
}
void Insert(node* pos, node* tmp)
{
tmp->front=pos;
tmp->next=pos->next;
//这里写的很美
tmp->next->front=tmp;
tmp->front->next=tmp;
}
3.2 头结点的其他意义
前面的概括有失偏颇,头结点维持形式统一其实是个很重要的意义,因为对于很多操作,很多时候是想不到要分类讨论,一个好的形式可以大大简化编程时在细节方面的注意度。而且对于一些题(基本上除了约瑟夫问题外的所有题),循环链表也是需要一个切入点的,一个可以告诉我们到底哪个是起始元素的点的。
因此总结一下头结点或者头指针的作用:
- 防止数据丢失
- 维持形式不变性
- 方便找到起始点
3.3 双向链表必然是循环链表
对于一个单链表,要维持形式不变性只需要一个头结点就够了,但是对于双向链表来说,尾结点也是头结点,只有一个头结点还是不能保证在尾部进行插入、删除时的形式不变性,所以真正重要的是形式不变性,而非某个节点。
Insert(node* pos, node* tmp)
{
tmp->front=pos;
tmp->next=pos->next;
tmp->front->next=tmp;
//没有尾结点的代价
if(tmp->next != NULL)
{
tmp->next->front=tmp;
}
}
这是一个插入操作,可以看到没有尾结点是需要分类讨论的。
那如果加上一个尾结点end好不好呢?我认为是不好的。
Delete(int id, node* head);
这是一个Delete函数声明,可以看到我需要把头结点传进去,才能进行操作,如果end跟head等价,那么我一定需要传end和head进去,显然是不简洁的。
解决这个问题的方法很简单,就是用头结点代替尾结点。这样就满足了形式不变性的要求,还保证了简洁,只是判断时不再是NULL判断,而是cur != head的判断。
3.4 用数组模拟链表
现在采用循环双向链表还是不能保证不TLE,这是因为链表的一个缺点就是查找时间过多,而这道题刚好要大量的查找,那么有什么办法解决这件事呢?其实用数组就可以了。正如我前面提到的,课本教授的数据结构只是例子,真正的数据结构是可以融合多种例子的优点的,就比如说这个,学生的号码就可以作为数组下标,进而可以随机查询,然后我可以用数组下标来代替指针地址,这样也可以利用链表的优点。这种思想在树中也有体现。另外,要是编号过大,可以用取模操作使编号减小,其实就是把数组升级成了哈希表。完整代码如下。
#include<stdio.h>
#include<math.h>
#include<ctype.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
#include<assert.h>
typedef struct node
{
int front;
int next;
}node;
node student[200000];
void Insert(int pos, int tmp, int dir)
{
if(dir == 0)
{
student[tmp].next=pos;
student[tmp].front=student[pos].front;
student[student[pos].front].next=tmp;
student[pos].front=tmp;
}
else
{
student[tmp].front=pos;
student[tmp].next=student[pos].next;
student[student[pos].next].front=tmp;
student[pos].next=tmp;
}
}
void Delete(int pos)
{
student[student[pos].next].front=student[pos].front;
student[student[pos].front].next=student[pos].next;
student[pos].flag=1;
}
void Print()
{
int i;
for(i=student[0].next; i!=0; i=student[i].next)
{
if(student[i].flag == 0)printf("%d ",i);
}
}
int main()
{
//freopen("input.txt","r",stdin);
int n,m;
int pos,dir;
scanf("%d",&n);
student[0].front=0;
student[0].next=0;
Insert(0,1,1);
for(int i=2; i<=n; i++)
{
scanf("%d%d",&pos,&dir);
Insert(pos, i, dir);
}
scanf("%d",&m);
while(m--)
{
scanf("%d",&pos);
Delete(pos);
}
Print();
//fclose(stdin);
return 0;
}