深入浅出数据结构C语言版(4)——表与链表

  在我们谈论本文具体内容之前,我们首先要说明一些事情。在现实生活中我们所说的“表”往往是二维的,比如课程表,就有行和列,成绩表也是有行和列。但是在数据结构,或者说我们本文讨论的范围内,我们所说的“表”是一维的,即所有“元素”都是前后排列的。就我个人而言,这样的“表”用“队列”来形容比较恰当。但是,数据结构中“队列”这个名词是被一种特殊的“表”给占用了的,所以我们没法再用“队列”来表示“表”,后期我们会对“队列”进行介绍。

  

  在第一段的说明过程中,我们其实已经说出了“表”的定义,就是若集合中的元素是一前一后像队伍一样联系起来的,那么这个集合就是一个表。

  显然,我们常用的数组就是表的一种,它符合“元素一前一后联系起来”的要求。表是最基础的数据结构(虽然结构体也能存多个数据在内,但其整体还是视为一个对象的),每当我们需要存储的数据不存在特殊的关系(比如以后会说的一对多关系),或者存在的关系为“一前一后”时(比如用户输入多个字符,字符间就存在输入顺序上的“一前一后”关系。在应用上,比如我们可以将字符存储进字符数组,然后判断用户输入的是不是正确的密码等),我们就可以使用表。

  那么,表的相关知识讲到这儿就算结束了吗?(毕竟数组大家应该都会用了)显然不是,我们说了,数组是表的一种,但也只是表的一种啊~

  接下来我们就要说说,使用数组作为表的缺点。首先让我们假设一种情况(这个在深入浅出数据结构(1)中提到过):

  我们要写一个程序,这个程序的过程很简单:获取用户输入(这里就需要用到表来存储用户的输入),对用户输入进行计算或操作,输出结果。

  但是这个程序要考虑一个情况:用户有时候只需要输入几个数据,而有时候却需要输入几万个数据(可能有人会说怎么可能输入那么多,人都累死了。。。但是别忘了有一种操作叫文件重定向,虽然我记不得也没用过╮(╯_╰)╭)。对于C程序来说,这个问题是不得不考虑的,如果你决定使用数组来存储用户的输入,那么数组的大小在创建之时就要确定好(印象中C11标准允许数组创建时的大小不再是固定的,但总是用最新的标准来写程序并不是什么好事,因为标准的普及总是要时间的)。很显然,数组的大小很好确定,比如十万(反正绝对大于用户可能的输入数量就行)。但是这会带来一个很严重的问题,就是你的程序在不需要那么多空间的情况下依然会使用那么多空间,比如用户平均输入只有几百个,然而每次运行程序都先占用了十万大小的数组空间。显然这样的程序很不好,一来用户的内存可能还有别的程序需要使用,二来可能别的用户不需要十万个的存储空间而且他的内存也不够。

  那么对于这种情况,我们该怎么办呢?

  首先我们发现我们依然要用到表(假设用户的输入数据间的关系就是一前一后),所以我们先确定下来使用的是表类型的数据结构。然后我们希望的关键点其实就是:表能够随着需要而改变大小。那么我们该如何实现这样一个表呢?现在,不需要你去思考解决方案了,我们已经有了现成的数据结构,那就是链表!

  回顾数组,我们发现,其实我们知道的信息有两个:

  1.数组第一个元素在哪

  2.数组中元素都是相邻的,后一个元素就在前一个元素的后面,中间没有“空”

  因为我们知道这两个信息,所以我们不需要知道各个元素的具体位置,只要根据某个元素在表中的顺序位置n,就可以由第一个元素的位置加上n来得到该元素。

  但这也正是数组的不足所在,为了满足第2点,我们必须在创建数组时就指定其大小,只有这样我们才能找到满足2的一整块区域给数组。那么,为了解决这个不足,我们要做的就是不再使用一整块区域,各元素自取所需(即元素不一定是相邻的了,只要添加元素时找一个可以放下该元素的容身之所就行)!

  但这又会带来一个问题,就是大家都分散开了,又如何找到大家呢?

  让我们再次回顾数组的信息2,然后进行另一个角度的解释:

  数组中某个元素的位置其实是根据其前一个元素的位置+1来获得的,而第一个元素的位置则需要我们手动保存。换句话说,某个元素的位置是其前一个元素“告诉”我们的!

  让我们把这一想法应用到我们的新想法中!即令每一个元素“记住”其下一个元素的位置!这样一来,我们就可以根据需要来向表中添加元素了,只要加入新元素后,原先的最后一个元素“记住”新元素在哪就可以令这个表像锁链一样“链”起来!我们的“链表”思想也就诞生了~:-P

  接下来我们要做的,就是将这个想法付诸实现了。相信很快,我们就会发现,实现链表的关键之处在于如何“令元素记住下一个元素的位置”?在这里我们要多嘴一句,正如第一篇博文所说的,数据结构不仅决定如何存储数据,有时候也要决定或者说不得不考虑“存储什么数据”。现在我们就遇上了这样的问题。显然,数据本身是不会记住下一个数据在哪的,比如数据类型为int,它又如何去记住下一个int在哪?这时候,我们就需要对数据“封装”一下,形成一种新的数据类型,然后这种数据类型要能够记住下一个相同数据类型的位置。

  思路已经出来了,就是“封装”出新的数据类型,然后要求其能记住下一个相同数据类型数据的位置。“封装”可以令我们想到结构体(也许你想不到但没关系,现在知道了╮(╯_╰)╭),而“位置”则会让我们想到指针。因此我们“封装”出来的新数据类型应该是一个类似这样的结构体:

struct  Element
{
      DataType  data;   //DataType根据实际元素类型决定
      struct  Element  *  next;   //next意味着其存储下一个元素的位置
}

  然后在我们的程序中,我们只需要知道第一个元素在哪就行了(第一个会告诉我们第二个在哪,以此类推):

struct  Element  a={data,NULL};
struct  Element  * List=&a;

//然后向这个以List开头的链表添加元素

  现在,对于为什么要使用链表,以及链表该如何实现的基本思想及如何“封装”出链表的元素我们应该都明白了。

  但是虽然知道了链表添加新元素时该怎么做已经有了口头上的描述,却还没有给出代码上的实现,并且如何从链表中删除一个元素以及如何找到第n个元素也还没给出实现。

  这些可以统称为对链表的操作,也可以说是能对链表这种数据结构使用的算法。关于这些,我们将在下一次博文中介绍。

  最后总结,我们这次讲了哪些呢?

第一,什么是表。

第二,表有什么可能的用处。

第三,数组作为表的缺点。

第四,引出链表的想法以及基本思路、基本元素的“封装”

  下一次博文,我们将介绍链表的操作,以及特殊的“链表”——游标数组

时间: 2024-10-26 21:29:38

深入浅出数据结构C语言版(4)——表与链表的相关文章

深入浅出数据结构C语言版(5)——链表的操作

上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游标数组--我决定还是给它单独写个博文比较好~). 那么,我们的过程应该是怎么样的呢?首先当然是分析需要什么操作,然后再逐一思考该如何实现,最后再以代码的形式写出来. 不难发现,我们希望链表能支持的(基础,可以由此延伸)操作就是: 1.给出第n个元素 2.在第n个元素的后面插入一个元素(包含在最后一个

深入浅出数据结构C语言版(9)——多重表(广义表)

在深入浅出数据结构系列前面的文章中,我们一直在讨论的表其实是"线性表",其形式如下: 由a1,a2,a3,--a(n-1)个元素组成的序列,其中每一个元素ai(0<i<n)都是一个"原子","原子"的意思就是说元素本身是一个个体,所有元素都是相同的结构. 但是在我们常见的某些应用,比如Excel的表格中,我们发现表并不一定是线性表,Excel中的表就明显是二维的结构 那么在数据结构中,我们会使用这种广义上的表吗?答案是会,我们也会.或

深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #define SIZE 1000 //表达式的单个元素所使用的结构体 typedef struct elem { int num = 0; //若元素存储操作数则num为该操作数 char oper = '='; //若元素存储操作符则oper为该操作符 bool IsNum = false; //用于

深入浅出数据结构C语言版(12)——从二分查找到二叉树

在很多有关数据结构和算法的书籍或文章中,作者往往是介绍完了什么是树后就直入主题的谈什么是二叉树balabala的.但我今天决定不按这个套路来.我个人觉得,一个东西或者说一种技术存在总该有一定的道理,不是能解决某个问题,就是能改善解决某个问题的效率.如果能够先了解到存在的问题以及已存在的解决办法的不足,那么学习新的知识就更容易接受,也更容易理解. 万幸的是,二叉树的讲解是可以按照上述顺序来进行的.那么,今天在我们讨论二叉树之前,我们先来讨论一种情形.一种操作:假设现在有一个数组,数组中的数据按照某

深入浅出数据结构C语言版(15)——优先队列(堆)

在普通队列中,元素出队的顺序是由元素入队时间决定的,也就是谁先入队,谁先出队.但是有时候我们希望有这样的一个队列:谁先入队不重要,重要的是谁的"优先级高",优先级越高越先出队.这样的数据结构我们称之为优先队列(priority queue),其常用于一些特殊应用,比如操作系统控制进程的调度程序. 那么,优先队列该如何实现呢?我们可以很快给出三种解决方案. 1.使用链表,插入操作选择直接插入到表头,时间复杂度为O(1),出队操作则遍历整个表,找到优先级最高者,返回并删除该结点,时间复杂度

深入浅出数据结构C语言版(14)——散列表

我们知道,由于二叉树的特性(完美情况下每次比较可以排除一半数据),对其进行查找算是比较快的了,时间复杂度为O(logN).但是,是否存在支持时间复杂度为常数级别的查找的数据结构呢?答案是存在,那就是散列表(hash table,又叫哈希表).散列表可以支持O(1)的插入,理想情况下可以支持O(1)的查找与删除. 散列表的基本思想很简单: 1.设计一个散列函数,其输入为数据的关键字,输出为散列值n(正整数),不同数据关键字必得出不同散列值n(即要求散列函数符合单射条件) 2.创建一个数组HashT

深入浅出数据结构C语言版(19)——堆排序

在介绍优先队列的博文中,我们提到了数据结构二叉堆,并且说明了二叉堆的一个特殊用途--排序,同时给出了其时间复杂度O(N*logN).这个时间界是目前我们看到最好的(使用Sedgewick序列的希尔排序时间复杂度为O(N4/3),下图为两者函数图像对比,但是注意,这并不是希尔排序与堆排序的对比,只是两个大O阶函数的对比).这篇博文,我们就是要细化用二叉堆进行排序的想法,实现堆排序. 在介绍优先队列的博文中(http://www.cnblogs.com/mm93/p/7481782.html)所提到

深入浅出数据结构C语言版(17)——有关排序算法的分析

这一篇博文我们将讨论一些与排序算法有关的定理,这些定理将解释插入排序博文中提出的疑问(为什么冒泡排序与插入排序总是执行同样数量的交换操作,而选择排序不一定),同时为讲述高级排序算法做铺垫(高级排序为什么会更快). 在讨论相关定理之前,我们必须先掌握一个与顺序有关的概念:逆序数. 所谓逆序数,就是"逆序组合的个数",假设我们希望的顺序为从小到大(反之同理): 设有元素互异数列X0,X1,X2--Xn-1,(元素互异即数列中任取两数均不相等)从中任取两数作为组合(Xa,Xb),若a<

深入浅出数据结构C语言版(22)——排序决策树与桶式排序

在(17)中我们对排序算法进行了简单的分析,并得出了两个结论: 1.只进行相邻元素交换的排序算法时间复杂度为O(N2) 2.要想时间复杂度低于O(N2),算法必须进行远距离的元素交换 而今天,我们将对排序算法进行进一步的分析,这一次的分析将针对"使用比较进行排序"的排序算法,到目前为止我们所讨论过的所有排序算法都在此范畴内.所谓"使用比较进行排序",就是指这个算法实现排序靠的就是让元素互相比较,比如插入排序的元素与前一个元素比较,若反序则交换位置,再比如快速排序小于