链表再讲----数组模拟链表


算法导论上有关链表讲的已经很好了,总结一下。

1.链表实现栈和队列

单链表为例:

栈的话就是先入先出,入的元素插在链表尾部,出的元素从链表尾部删除。

Push :insert(L,n)。
Pop : delete(L,n)。

队列的实现与栈类似,入的元素插在链表尾部,出的元素从链表头部删除。

Push :insert(L,n)。
Pop :delete(L,0)。

2.模拟链表

C/C++可以直接操作内存,因为它们有指针的概念,但是诸如java等高级语言并没有指针,那么对于这些无法直接操作内存的语言,如何实现链表呢?
接下来介绍两种方法:

#1 多数组表示法

在单链表中,链表的每一个结点均包含数据域与指针域,我们可以开创两个数组,其中数组data充当数据域,数组next充当指针域。

data存放初始数据,next存放索引,比如初始序列{1,2,3,4,5},那么对应有data[1]=1.data[2]=2,…data[5]=5,并且初始化next[1]=2,next[2]=3…next[5]=0,next[i]表示data[i]的下一个元素的索引。我们令next[n]=0,表示data[n]没有下一个元素。

以上是初始化,这段叙述用代码实现并不难,如下:

void initList(int n)
{
	  for(int i = 1; i <= n; i++)
	  {
   		  scanf("%d",&data[i]);
	  }
	  for(int i = 1; i <= n; i++)
	  {
		    if(i != n)
			     next[i] = i + 1;
		    else
			     next[i] = 0;
	  }
}

遍历:

遍历链表也很简单,我们只要用next[i]不断获取data[i]的下一个元素的索引,不断输出直到判别出next[i]==0时停止。

void print()
{
	  int t = 1;
	  while(t != 0)
	  {
		    printf("%d ",data[t]);
		    t = next[t];
	  }
}

插入:

插入要难理解一些,但实现代码其实非常简洁。如果要插入一个元素,因为用指针实现的链表各结点是不连续的,因此可以轻易的在链表中的任意位置插入,但数组是连续的,我们用数组模拟的链表很显然无法在插入位置再放入一个元素。

那么新插入的元素放到哪里去呢?其实不难想到,对于一个长度为n的序列,在data数组里面存储的话那么数组里面前n个内存空间肯定已经被占据,这时我们新插入一个元素,使得序列长度变为n+1,那么这个新元素肯定要放在也只能放在data数组里面第n+1个内存位置去。那么对应的代码就是data[++n]=x,x为新插入的元素。

只是纯粹的令data[++n]=x还不行,因为按照我们上面写的遍历链表的方法,遍历在第新插入元素的前一个位置就会停止,为了保证链表的逻辑与结构完整,我们设定第n-1个位置上的元素(++n之后n值已经变化,n-1即未插入前序列的最后一个位置)仍然为该链表的最后一个元素。

假定我们要把新元素插入在第pos位,那么我们需要考虑的就是如何把pos位前的链表与pos位后的链表重新连在一起。首先我们想到,pos位前的链表(记为front链表)仍然是有序连接的,front链表的最后一个元素是第pos-1位的元素,它原先保存的next值,即next[pos-1]保存的是pos位后的链表(记为rear链表)的第一个元素的索引,但现在我们要使next[pos-1]保存的是新插入元素的索引,因为新插入的元素一定是在第n个位置,所以有next[pos-1] = n

这算是完成了连接的一半,next[pos-1] = n使front链表与新插入元素连接在了一起,但新插入元素还没有和rear链表连接,想一想让新插入元素与rear链表连接的语句怎么写?其实非常简单,想要连接rear链表,必须要知道rear链表的第一个元素的索引,因为next[pos-1]原先保存的就是这个索引,所以我们只需让next[n] = next[pos-1]即可。这时你就又发现了一个问题,因为next[pos-1]在上一步已经被更改了啊,这就更好解决了,因为我们的逻辑推理是先把前面的连接,之后再连接后面的,那么我们在代码实现中先连接后面的就好了。

切记连接rear链表的语句是next[n] = next[pos-1],不要想当然的写成next[n] = pos,因为你无法保证插入操作只会进行一次,如果某个序列需要多次插入,那么当第二次插入时,next[n] = pos就已经错了。

总结以上所述,得到我们的代码实现部分:

void insertList(int pos,int x,int &n)
{
	   data[++n] = x;
	   next[n] = next[pos-1];
	   next[pos-1] = n;
}

别看文字吧啦吧啦了那么多,真正的代码其实就这几行而已。

删除:

删除比插入要好理解一些,我们要删除一个元素,只需把该元素前面的链表与后面的链表连接在一起即可。
按位置删除的话,那么对应的代码就是next[pos-1] = next[pos];

按值删除的话,那么需要先找到与该值对应的pos,之后的处理都是一样的。

代码实现:

void deleteList(int pos)
{
	   next[pos-1] = next[pos];
}

简洁的要死。

至于查询啦、修改啦,合并两个链表啦什么的,我就不说了,只要上面的这些操作掌握了,那么剩下的其它操作都不在话下。

再提醒一点,data数组和naxt数组切记定义时要初始化的大一点,如果你嫌空间浪费,那么用vector也可以,求长度什么的也更方便些。

小结:

用多数组模拟的链表比之用指针实现的链表在代码上要简洁不少,而且也能很好的实现链表的特性,甚至比指针链表犹有过之,可以看到上例中的模拟链表在插入和删除操作上都是O(1)的复杂度。
用这种方法我们也可以实现双链表与循环链表,感兴趣的可以自己尝试,我就不再多说了,全部都给说明的话,篇幅太长,相信你们看的也烦。

#2 单数组表示法

接下来介绍模拟链表的第二种方法,单数组表示法。

其实我觉得定义一个结构体数组也能用来说明问题,嘛,不过不用结构体也不是不能实现,而且用结构体会局限我们的思维。
先说一下它的思想吧,我们把一块连续的内存空间分成多个区,其中每个区包含多个域,如果是单链表那么它就包含两个域,next指针域与数据域,如果是双链表那么它就包含三个域,数据域、next域与pre域。我们知道对于一个数组来说,数组名代表首地址,之后我们可以用数组名+x的形式得到偏移量。比方对于一个区A[j…k],它是数组A[n]的一个子数组,那么对于给定的指针j,A[j+1]、A[j+2]分别可以得到它的pre域以及next域。

这种单数组表示法比较灵活,因为它允许不同长度的对象存储在同一数组中,即管理一组异构对象,这要比管理一组同构对象要困难得多,我们之前所学大部分也均是同构对象。如果用结构体的话,则等于又将思维限制在了只能用来存储同构对象的局限中。

对于同构对象来说,单数组表示法倒还可写,以双链表为例,从lsit[1]开始,每连续的三个内存空间分别保存当前结点的数据域、next域与pre域,但这种方法说实话还不如直接用多数组表示来的清晰明了。

对于异构对象来说,尽管单数组是可以实现的,但因为书上没给过多说明,只是一笔带过,在网上也几乎搜不到这方面的东西,因此我也是毫无头绪,以后看到相关资料的话再找机会补上吧,当然前提是我不忘的情况下。

这里就先了解一下它的思想好了。



3.链表推广–有根树的表示

在前面的讲解以及包括上一篇博客中我们主要讲的是如何来实现链表,包括指针法和数组法,实际上这些方法可以推广到任意同构的数据结构上,而尤其在树上应用最广。树的结点用对象表示,这点和链表类似,对每一个树结点,除了数据域外,其它我们感兴趣的属性包括指针,会随着树的种类而有所变化。

二叉树:

对于一个结点,它有三个指针域p,left,right,其中left指向该结点的左孩子,right指向该结点的右孩子,而p指向该结点的父亲结点。如果left、right指针域为空则表示到达叶子结点,如果p为空则表示该结点为根结点,同时我们定义一个指针root,它指向根结点,如果root为空,则表示该二叉树为空。

我们用这种方式可以推广到每个结点至多为常数k的任意类型的树上,但对于结点数无限制的树,这种方式就会失效,此外,即使我们可以用这种方式来表示一个常数k很大的树,但若多数结点只有少量的孩子,则会造成内存空间的大量浪费。

分支无限制的树:

鉴于二叉树表示法的一些诟病,怎么来表示分支无限制的树呢?这里有一种巧妙的方法,既不会浪费内存空间,也能很好的照顾到每一个结点,而且在内存损耗上也并不大。这种方法同样用到指针域p、left、right,指针p仍指向父亲结点,指针left仍指向左孩子,只不过因为孩子可能很多的缘故,规定指针left指向的是最左边的孩子,唯一不同的是指针right,指针right不再指向它的孩子结点,而是指向该结点右边的兄弟结点。

这样一来,如果left为空,则说明该结点无孩子结点,如果right为空,则说明该结点是其父亲结点的最右孩子。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值