数据结构笔记
1.1 概述
数据结构是计算机存储、组织数据的方式,它是指相互之间存在一种或多种特定关系的数据元素的集合。
数据结构包含三个方面:
1、逻辑结构:数据元素之间存在的关系叫做数据的逻辑结构
2、物理结构(存储结构):数据结构在计算机中的表示(映象)叫数据的物理结构
3、运算
逻辑结构:数据元素之间存在的(逻辑)关系叫做数据的逻辑结构
- 集合:没有关系
- 线性:一对一关系,有唯一的首元素和尾元素,除此之外每个元素只有唯一的前驱和后继
- 树形:一对多的关系,每个元素的后继的个数是不确定的,可以有多个(大于等于0),除了根结点之
外,其余所有结点都只有唯一的前驱 - 图形(网状):多对多的关系,每个结点都可以多个前驱,也可以有多个后继体现的是多对多的关系
存储结构(物理结构):数据结构在计算机中的表示(映象、存储)叫数据物理结构,或者叫存储结构
- 顺序存储:所有元素按物理顺序依次存储在一片连续的空闲存储单元中
- 链式存储:数据元素可以存储在一片连续的单元,也可以存储在一片不连续的单元中
比如采用顺序存储的方式以及链表的方式来存储字符串“ABC”,见下图:
- 顺序存储的方式存储单元中的数据的占用率达到100%,也就是说所有存储单元都是用来存储数据:
- 存储单元中的数据的占用率达不到100%,也就是说所有存储单元不全是用来存储数据,有部分存储单
元将用于存储指针。
- 索引:使用索引存储的方法存储元素的同时,还需建立相应的索引表,索引表中的每一项称为索引项。
- 散列:散列存储方式通过构造散列函数,用函数的值来确定数据元素存放的地址。比如LOC(k)=k%7,其中,k为各结点数值的ASCII值,现在需要把abcde这几个字符存入到一个长度为7的哈希表里,如图:
1.2 时间复杂度
1、如果代码段中没有循环,则时间复杂度一般为常数阶,即O(1)
2、如果代码段中存在循环,则时间复杂度由循环次数最多的那一段代码决定
3、计算出循环次数后,只保留最高阶项,且需除去最高阶项的常系数,即为最终的时间复杂度量级,如:循环次数为4n3+2n2-n/2+7, 因只保留最高阶项,所以+2n2-n/2+7将全部去掉,因需除去最高阶项的常系数,所以4n3的常系数4也需去掉,则最终的时间复杂度为O(n3)
代码段中循环次数最多的一段代码是单层循环时:
例1:
for(i=1;i<n;i++)
{}
循环次数是n-1次,时间复杂度为O(n)
例2:
for(i=0;i<=n;i++)
{}
循环次数是n+1次,时间复杂度为O(n)
例3:
for(i=0;i<=n/2;i++)
{}
循环次数是n/2+1次,时间复杂度为O(n)
例4:
for(i=0;i<=2*n;i++)
{}
循环次数是2n+1次,时间复杂度为O(n)
代码段中循环次数最多的一段代码是双层循环时:
例5:
for(i=0;i<n;i++){
for(j=0;j<n;i++){}
}
循环次数是n2次,时间复杂度为O(n2)
例6:
for(i=0;i<=n;i++){
for(j=0;j<=n;i++){}
}
循环次数是(n+1)*(n+1)=n2+2n+1次,时间复杂度为O(n2)
例7:
for(i=0;i<=2n;i++){
for(j=0;j<=2n;i++){}
}
循环次数是(2n+1)*(2n+1)=4n2+8n+1次,时间复杂度为O(n2)
例8:
for(j=1;j<n;j=2*j){
}
循环次数是logn次,时间复杂度为O(logn)
例9:
for(i=0;i<=n;i++){
for(j=1;j<n;j=2*j){}
}
循环次数是(n+1)*logn=nlogn+logn次,时间复杂度为O(nlogn)
例10:
for(i=0;i<m;i++){
for(j=0;j<n;j++){}
}
循环次数是mn次,时间复杂度为O(mn)
注意:如不做特殊说明,所讨论的各算法的时间复杂度均指 最坏情况下的时间复杂度 。
1.3 线性链表
线性结构:每个元素最多只有一个出度和一个入度,表现为一条线状。线性表按存储方式分为 顺序表和
链表 。
存储结构:
• 顺序存储: 用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素物理上也相邻。
• 链式存储: 存储各数据元素的结点的地址并不要求是连续的,数据元素逻辑上相邻,物理上分开。
1.4 栈和队列
队列、栈也是线性结构,结构如下图,队列是先进先出,分队头和队尾;栈是先进后出,只有栈
顶能进出。
循环队列中,头指针指向第一个元素,尾指针指向最后一个元素的下一个位置,因此,当队列空时,head=tail,当队列满时,head=tail,这样就无法区分了,因此,一般将队列少存一个元素,这样,队列满时的条件就变为tail+1=head,而考虑是循环队列,必须要除以最大元素数来取余数,即(tail+1)%size=head,如上图右边所示两个公式。循环队列的长度公式为(Q.tail-Q.head)%size。优先队列;元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。使用堆来存储因为其不是按照元素讲队列的顺序来决定的。
1.5 矩阵结构
特殊矩阵:矩阵中的元素(或非0元素)的分布有一定的规律。常见的特殊矩阵有 对称矩阵、三角矩阵和对角矩阵 。
稀疏矩阵:在一个矩阵中,若非零元素的个数远远少于零元素个数,且非零元素的分布没有规律。
存储方式为三元组结构,即存储每个非零元素的(行,列,值)。
1.6 树
树的基本概念如下:
(1)双亲、孩子和兄弟:结点的子树的根称为该结点的孩子;相应地,该结点称为其子结点的双亲。具有相同双亲的结点互为兄弟。
(2)结点的度:一个阶段的子树的个数记为该结点的度。
(3)叶子结点:叶子结点也称为终端结点。指度为0的结点。
(4)内部结点:度不为0的结点,也称为分支结点或非终端结点。除根结点以外,分支结点也称为内部结点。
(5)结点的层次:根为第一层,根的孩子为第二层,以此类推,若某结点在底i层,则其孩子结点在第i+1层。
(6)树有高度:一棵树的最大层数记为树的高度(或深度)。
(7)有序(无序)树:若将树中结点的各子树看成是从左到右具有次序的,即不能交换,则称该树为有序树,否则称为无序树。
1.6.1 二叉树
满二叉树:每层都是满结点的
完全二叉树:k-1层是满结点的,第k层结点从左到右是满的
二叉树的一些特性:
• 性质1:在二叉树的第i层最多有2(i-1)个结点(i>=1)。
• 性质2:深度为k的二叉树最多有2k-1个结点(k>=1)。
• 性质3:对任意一颗二叉树,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1。
• 性质4:具有n个结点的完全二叉树的深度为 logn+1。
• 性质5:如果根节点标号为1,则对任意结点i,它的左孩子为2i,右孩子为2i+1。
二叉树的遍历
一颗非空的二叉树由根节点、左子树、右子树三部分组成,遍历这三部分,也就遍历了整颗二叉树。这三部分遍历的基本顺序是先左子树后右子树,但根节点顺序可变,以根节点访问的顺序为准有下列三种遍历方式:
• 先序(前序)遍历:根左右。
• 中序遍历:左根右。
• 后序遍历:左右根。
还有层次遍历方法:
• 从上到下
• 从左到右
示例:前序:12457836中序:42785136后序:48752631
索引二叉树
引入线索二叉树是为了保存二叉树遍历时某节点的前驱节点和后继节点的信息,二叉树的链式存储只能获取到某节点的左孩子和右孩子结点,无法获取其遍历时的前驱和后继节点,因此可以在链式存储中再增加两个指针域,使其分别指向前驱和后继节点,但这样太浪费存储空间,考虑下述实现方法:若n个节点的二叉树使用二叉链表存储,则必然有n+1个空指针域,利用这些空指针域来存放节点的前
驱和后继节点信息,为此,需要增加两个标志,以区分指针域存放的到底是孩子结点还是遍历节点,如下:
最优二叉树
若二叉树的二叉链表采用上述结构,则称为线索链表,其中指向前驱、后继节点的指针称为线索,加上线索的二叉树称为线索二叉树。
最优二叉树又称为哈夫曼树,是一类带权路径长度最短的树,相关概念如下:
- 路径:树中一个结点到另一个结点之间的通路。
- 结点的路径长度:路径上的分支数目。
- 树的路径长度:根节点到达每一个叶子节点之间的路径长度之和。
- 权:节点代表的值。
- 结点的带权路径长度:该结点到根结点之间的路径长度乘以该节点的权值。
- 树的带权路径长度(树的代价):树的所有叶子节点的带权路径长度之和。
查找二叉树
查找二叉树上的每个节点都存储一个值,且每个节点的所有左孩子结点值都小于父节点值,而所有右孩子结点值都大于父节点值,是一个有规律排列的二叉树,这种数据结构可以方便查找、插入等数据操作。二叉排序树的查找效率取决于二叉排序树的深度,对于结点个数相同的二叉排序树,平衡二叉树的深度最小,而单枝树的深度是最大的,故效率最差。
平衡二叉树
前面讲过查找(排序)二叉树,特点是所有左子树值小于根节点值,所有右子树值大于根节点值,而这个特点可以构造出多个不同的二叉树,并不唯一,因此提出平衡二叉树的概念,在查找二叉树特点的基础上,要求每个节点的平衡度只能为0或1或-1.节点的左右子树深度就是其左右子树各自的层数,而后将左子树深度减去右子树深度,就得到了该节点的平衡度。因此,平衡二叉树就是任意左右子树层次相差不超过1
图的存储
邻接矩阵:使用二维数组来表示图中节点之间的连接关系。
• 第i行和第j列的元素表示节点i和节点j之间是否有连接。
• 空间复杂度较高,但查找连接关系效率高。
• 当图较密集时,邻接矩阵更好
邻接链表:用到了两个数据结构,先用一个一维数组将图中所有顶点存储起来,而后,对此一维数组的每个顶点元素,使用链表挂上和其有连线关系的结点的编号和权值,示例如下图所示:空间复杂度较低,增加节点效率高,但查找连接关系效率低,当图较稀疏时,邻接链表更好
图的最小生成树
假设有n个节点,那么这个图的最小生成树有n-1条边(不会形成环路,是树非图),这n-1条边会将所有顶点都连接成一个树,并且这些边的权值之和最小,因此称为最小生成树。
• 普里姆算法(Prim):从任意顶点出发,找出与其邻接的边权值最小的,此时此边的另外一个顶点自动加入树集合中,而后再从这这个树集合的所有顶点中找出与其邻接的边权值最小的,同样此边的另外一个顶点加入树集合中,依次递归,直至图中所有顶点都加入树集合中,此时此树就是该图的最小生成树。
• 克鲁斯卡尔算法(Kruscal,推荐):这个算法是从边出发的,因为本质是选取权值最小的n-1条边,因此,就将边按权值大小排序,依次选取权值最小的边,直至囊括所有节点,要注意,每次选边后要检查不能形成环路。
这两种算法都是局部最优原则,所以都是贪心法算法,并且没有谁的效率高谁的效率差,因为克鲁斯卡尔算法是数边的,所以边越多,它算起来越麻烦
查找和排序-算法基础
所谓算法就是指解题方案的准确而完整的描述,简单的说就是某个问题的解题思路算法的复杂度:
• 时间复杂度是指程序运行从开始到结束所需要的时间。
• 空间复杂度是指对一个算法在运行过程中临时占用存储空间大小的度量。一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小常见的对算法执行所需时间的度量:
O(1)<O(log2n)<O(n)<O(nlog2n)<O(n²)<O(n³)<O(2ⁿ)<O(n!)
上述的时间复杂度,经常考到,需要注意的是,时间复杂度是一个大概的规模表示,一般以循环次数表示,O(n)说明执行时间是n的正比,另外,log对数的时间复杂度一般在查找二叉树的算法中出现。渐进符号O表示一个渐进变化程度,实际变化必须小于等于O括号内的渐进变化程度。
二分查找
折半(二分)查找:设查找表的元素存储在一维数组r[1…n]中,在表中元素已经按照关键字递增方式排序的情况下,进行折半查找的方法是:
1、首先将待查元素的关键字(key)值与表r中间位置上(下标为mid)记录的关键字进行比较,若相等,则查找成功;
2、若key>r[mid].key,则说明待查记录只可能在后半个子表r[mid+1…n]中,下一步应在后半个子表中查找;
3、若key<r[mid].key,说明待查记录只可能在前半个子表r[1.mid-1]中,下一步应在r的前半个子表中查找;
4、重复上述步骤,逐步缩小范围,直到查找成功或子表为空失败时为止。要注意两点:中间值位置求出若为小数,应该向下取整,即4.5=4,非四舍五入;中间值已经比较过不相等,在划分下一次比较区间时,无需将中间值位置再纳入下一次比较区间。当查找的数据越多时,二分查找的效率越高。
折半查找的时间复杂度为 O(log2n)