先来一张总体的结构图:
线性表的定义:
始终记住一点,任何的数据结构,都逃不过逻辑结构,物理结构(存储结构),以及数据的运算这三个部分。这是数据结构的规定。
线性表的逻辑结构:
线性表是具有相同数据类型的n个数据元素的有限序列,注意:n是>=0的,也就是说,n可以等于0,当n等于0的时候,这个线性表就是一个空表。
怎么理解这个定义呢?有三点重要!
首先是具有相同的数据类型:也就是每一个数据元素的类型都是相同的,我们把线性表的每一个数据元素称之为节点:
其次是有限的,也就是线性表不能是无限长度的,那么既然是有限的,肯定就存在首尾,所以第一个叫表头元素,最后一个叫表尾元素。
最后一个就是有序的,就好此学生排队一样,排成一列纵队,那么他们的顺序一定是清除的,谁在谁的前面,谁在谁的后面。这都是能够确定的。同时第一个节点只有之后有节点,最后一个节点之后它的前面有节点。
-------------------------------------------------
线性表的物理结构——顺序存储结构 (顺序表)
顺序存储是用一组地址连续的存储单元,依次存储线性表中的数据元素。顺序存储的线性表也叫做顺序表。 要注意到:他们的物理位置也是相邻的。
那么建立顺序表需要考虑三个因素:
1.存储空间的起始位置:也就是第一个节点从什么地方开始
2.顺序表的最大存储容量,那这个容量肯定不能超过空间的存储容量呀,因为超过了根本装不下。
3.顺序表当前的长度
静态分配:
上面这个图实际上就已经是定义了一个完整的顺序表。有起始位置,最大容量,和顺序表的长度 ,不过上面这个是属于静态分配。怎么说呢?就是一开始我们就已经把大小是限定死了的,比如说这里的容量就只有50,一定好之后就没有办法修改了。
动态分配:顺序表的容量有程序动态的进行分配
顺序表的操作:
插入操作:
比如上面的这个图所表示的,我们要把5插入到1和2之间,那么要怎么插入呢?这里应该是把后面的234都向后移动一位,空出一个位置来,然后才能吧5放到1和2之间! 那有疑问!为什么不把1的位置向前移动一个位置,这样不是更加高效嘛!一定要注意,这里的1就是表的开始位置,他是一个基准,是不能随便改变的。
另外要记住,在那个位置插入,就要把那个位置及其后面的元素统统的往后移动一个。所以插入的位置范围应该是从1~表长+1,那表长+1就是直接在最后一个元素的后面插入就行了,就不用移动。
删除操作:
根据上面这个图,我们现在要删除掉第2个元素,那么删除的步骤应该是怎么样的呢? 首先应该把第2个元素拿掉,然后将3和4元素依次的向左移动一个位置。
顺序表的特点:
1.存储密度大,因为顺序表中逻辑上相邻的节点,他们在内存里面的物理位置也是相邻的,所以不用过多的空间去存储他们前后节点的位置信息。所以想要查询顺序表中的数据,那是相当快的。
2.插入和删除操作性能低,因为插入和删除都可能要移动大量的节点
3.因为顺序表对存储的要求是物理地址也是连续的,那么久很有可能造成很多小的空间无法被使用,这些小空间我们叫碎片。
-------------------------------------------------
线性表的物理结构——链式存储结构 (链表——单链表)
从链表开始,他就和顺序表有很大区别了,单链表的一个节点有两个部分,一个就是用来存本节点的数据的,也就是本节点所携带的重要数据,另外一个部分就是用来存放指向下一一个节点的指针的,这个指针有什么用呢? 他就是用来存放下一个节点的地址信息的,通过这个地址信息,节点之间就可以很容易的连接起来,并且知道下一个节点的身份。
那么要怎么表示单链表!very easy! 我们只需要找到这个单链表的头指针就行了。
那么头指针是什么!头指针就是指向单链表第一个节点的地址信息,因为每一个节点都存放了下一个节点的地址信息,所以我们只需要找到第一个节点,就可以很容易的找到整个单链表。
实际上单链表的第一个有效数据节点之前我们还可以添加一个节点,叫做头节点,它可以用来存储一下整个链表的总体信息,比如表长等。。但是一定要注意:头节点是可有可无的,它和头指针是有本质区别的,不管有没有头节点,头指针都指向链表的第一个节点,注意是第一个节点!!!也就是说,如果链表存在头节点,那么头指针就指向头节点,因为头节点也是携带了第一个有效数据节点的地址信息的。如果不带头节点,那么头指针就直接指向第一个带有效数据的节点,因为这个时候,第一个有效数据节点就是真个链表的第一个节点。 所以重中之重,头指针是必须存在的。
那么设置头节点有啥子好处呢?
1.处理起来方便呀,你看,我们都知道,如果是在数组里面的话,他的下标其实是从0开始的,和我们平时认知的从1开始比较用起来有点不舒服。但是一但加了头节点,那么真正有效数据的节点就是从第二个开始了,如果是在数组里面的话,那么他的下标也就是1了。
2.不管链表是否为空,头指针反正就是个指向头节点的非空指针,这样的话那基本就不存在空表了,因为你即使没有了有效数据节点,还有一个头节点在哪儿撑着呢。
单链表的操作:
建立单链表:
头插法建立单链表:
看清楚,头插法头插法,一定是在表的头头部分插进来,所以每插入一个元素都是插在了表头部分,而其他的元素也依次的往后移动。 很明显,头插法后插入的数据反而在这个单链表的前面。
尾插法建立单链表:
尾插法其实和头插法的算法都差不多,只不过尾插法的插入点是在链表的尾部,而不是头部
按照序号查找单链表的节点:
从单链表的第一个节点开始,顺着指针的next逐个的排查,直到找到对应的那个节点的序号为止,如果没有找到就为空
按照值查找链表中的节点:
同样是从第一个节点开始,由前往后依次比较节点中数据域的值,直到数据域的值等于给定的值。
插入操作:
比如将一个新的值x插入到单链表的第i个位置上,那么首先检查这个插入位置的合法性,然后找到这个节点的前驱节点,也就是它的前面一个节点,在它前驱节点的后面插入这个新的节点,至于怎么找到这个节点的前驱节点,可以参照我们刚才说的找节点的方法呀,找到对应的节点之后,再减1不久完事儿了吗?
删除操作:
思想其实都是和插入差不多的,重点在于我们对指针的运用,比如我要删除链表L的第i个节点,肯定首先还是要检查节点位置的合法性,然后我要找到这个指针的前面一个节点,以及它后面一个节点,最后一步就是将前面一个节点的后继指针指向我们删除的这个节点的后面一个节点。
线性表的物理结构——链式存储结构 (链表——双链表)
单链表有一个指针,那么很明显,我们可以通过名字就能想出来,双链表那肯定是有两个指针,而且一个指针指向的是前一个节点,一个指针指向的是后一个节点。
双链表的操作:
插入操作:有了前面的经验,我们很容易就可以想到,该怎么进行链表之间的插入,比如上面这里,我们想要把5插入到2和3之间,那么很显然就是要断开2和3之间的指针。然后再把指针的顺序指向5,至于怎么指,他有一个先后顺序。这个就自己搞定了。
删除操作:同上,注意算法上的思想,其实是一致的。
循环链表和静态链表:
循环单链表:其实这个非常好理解,什么是循环,就是首尾连接再一起那就是循环,所以循环单链表,其实就是末尾节点的指针不再是指向null,而是指向了头节点或第一个节点。上面没有说单链表的最后一个节点的指针是指向null哈,但是这个真容易想明白,最后一个节点之后就是没有节点嘛,这个很明显就是空呀。
那么很明显了,循环单链表是否为空的条件就是头节点的后继指针是否还是指向了头节点
循环双链表:很明显,双链表的循环其实也和上面的是一个性质,就是把头节点的前驱节点指向尾节点,尾节点的后继指针指向头节点,那么判断循环双链表的条件就是前后指针是否都指向了自己。
静态链表:大家都知道,再c和c++中都有指针这个概念,但是巧了,有些语言他就是没有指针,那怎么办嘛,那还是有办法来描述链表的,另外一种办法就是用数组来描述链表,而其中的后继和前驱指针都用数组中元素的下标来代替。