线性数据结构

线性数据结构

一、总论

​ 线性数据结构是极适用于模拟算法的一种数据结构。这么说好像不太精确,一方面,确实链表循环链表数组,都可以很精确的模拟某种背景,另一方面,线性数据结构,尤其是链表,又代表着某种抽象的数学思想,从而可以被应用到那些可能不是那么明显的题目中,比如递归BFSDFS哈希中。在进一步讲,任何一种数据结构或许都兼具模拟抽象两方面的应用,只是因为我现在刚学完线性表,所以对他感触较深罢了。

​ 正是因为如此,我现在学习的时候往往陷入误区:有的数据结构过于擅于模拟,所以我就不会思考用其他数据结构来对这道题进行模拟,比如约瑟夫问题只会用循环链表来写,但忽略了可以用队列的方法进行处理;有的数据结构擅于抽象,但是真的落实到代码上,过于抽象的思想,往往有细节上面的问题,如双向链表

​ 谈及数据结构,切不能以为这是一些固定的、可以稍微改动一下就能使的代码块。结构就是关系,每种数据结构大多只强调其中的一种关系,比如数组强调随机存储链表强调前后关系强调先进后出,但是如果想更快更好的解决问题,有两个方向。一个是改造数据结构,使其在保证原有结构思想的前提下功能更强大,比如辅助栈,另一个是将不同的结构思想进行合并,比如用数组实现链表和树,就兼具了相互关系随机存取的优点。


二、约瑟夫问题

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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值