读《数据结构》 5-6章[数组和广义表和树]

2016.06.30 – 07.12
读《数据结构》(严蔚敏 吴伟明)“5、6章”的个人笔记。

5 数组和广义表

07.04
本笔记的两种数据结构 —— 数组和广义表可以看成是线性表在下述含义上的扩展:表中的数据元素本身也是一个数据结构。

5.1 数组

(1) 数组的定义

数组抽象数据类型数组定义

在数据元素的逻辑关系中,每个非结尾元素都有一个后继元素,每个非最开始元素都有一个前驱元素。根据数组的定义,a[m][n][k]的数组元素总数为m x n x k,即对于n维数组,数组元素个数 ni=1bi bi 为数组第i维长度。n维数组的数据元素存储位置的计算公式(n维数组的映像函数)为(主要依据数组定义/数组元素枚举而来):

loc(j1,j2,,jn)=loc(0,0,,0)+(b2×b3×bn×j1+b3×b4××bn×j2++bn×jn1+jn)×L

L为每个数组元素所占存储单元个数。 bi 为数组第i维长度。

(数组一般不进行插入、删除元素操作 —— 复杂度)

n维数组类型为其 数据元素为n – 1维数组类型 的一维数组类型(的定义)

(2) 数组的顺序表示和实现

/* sequence_array.c * 数组的顺序表示描述和简单实现 * 2016.07.05 */
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>

#define MAX_ARRAY_DIM 8 // 规定数组维数最大值
#define OK 0 // 函数正常返回状态
#define NK 1 // 函数非正常返回

typedef char ElemType;      // 数组元素类型

/* 描述数组的结构体 */
typedef struct {
    ElemType    *base;      // 数组元素基址
    int         dim;        // 数组维数
    int         *bounds;    // 数组各维界基址
    int         *constants; // 数组映像函数常量基址
}Array;

/* 数组基本操作 */
// 构造相应合法的维数和各维度的数组
int init_array(Array *pa,int dim,...)
{
    int         i;
    va_list     ap;
    unsigned    elemtotal;  // 类型由动态分配最大量决定

    if (dim < 1 || dim > MAX_ARRAY_DIM) return NK;

    pa->dim     = dim;
    pa->bounds  = (int *)malloc(dim * sizeof(int)); // 保存各维长度的首地址
    if (!pa->bounds) exit(NK);

    // 求数组内存空间总大小
    elemtotal   = 1;
    va_start(ap,dim);
    for (i = 0; i < dim; ++i) {
        pa->bounds[i]   = va_arg(ap,int);
        if (pa->bounds[i] < 0) exit(NK);    // man va_arg()发生错误没说是小于0的错误
        elemtotal   *= pa->bounds[i];       // n维数组元素个数计算
    }
    va_end(ap);

    pa->base    = (ElemType *)malloc(elemtotal * sizeof(elemtotal));
    if (!pa->base) exit(NK);

    // 数组映像函数计算
    pa->constants   = (int *)malloc(dim * sizeof(int));
    if (!pa->constants) exit(NK);
    pa->constants[dim - 1]  = 1;    // 只以数组最右一列变化的数组元素直接数
    for (i = dim - 2; i >= 0; --i)
        pa->constants[i]    = pa->bounds[i + 1] * pa->constants[i + 1];
    return OK;
}

// 销毁数组
void destroy_array(Array *pa)
{
    if (!pa) return ;
    if (pa->base) {
        free(pa->base);
        pa->base    = NULL;
    }
    if (pa->bounds) {
        free(pa->bounds);
        pa->bounds  = NULL;
    }
    if (pa->constants) {
        free(pa->constants);
        pa->constants   = NULL;
    }
}

// 给数组指定元素赋值
int assign_array_value(Array *pa,ElemType e,...)
{
    int         i;
    int         arg;
    va_list     ap;
    unsigned    off;

    if (!pa) return NK;
    off = 0;
    va_start(ap,e);
    for (i = 0; i < pa->dim; ++i) {
        arg = va_arg(ap,int);
        if (arg < 0 || arg > pa->bounds[i]) return NK;
        off += pa->constants[i] * arg;  // 数组下标顺序要求是从左到右 - 得看constants存储数组下标对应
    }
    va_end(ap);
    *(pa->base + off)   = e;
    printf("%2d = %d ",off,e);
    return OK;
}


/* 简单测试数组的基本操作函数 */
int main(void)
{
    int     i,j,rv;
    int     dl,dr,dim;
    Array   a;

    dim = 2;
    dl  = dr = 5;
    init_array(&a,dim,dl,dr);
    for (i = 0; i < dl; ++i) {
        for (j = 0; j < dr; ++j) {
            assign_array_value(&a,i + j,i,j);
        }
        printf("\n");
    }
    destroy_array(&a);
    return 0;
}

思路
该段程序是根据数组的定义依据数组的下标(维数)给数组分配一段连续的空间;然后根据数组下标访问形式(含义)定位到相应的内存单元。

man变长参数
void va_start(va_list ap,last)
va_start()宏用以初始化ap以供后续的va_arg()和va_end()使用,它必须被最先调用。last是变参数列表前的最后一个参数名,也就是说,调用函数知道该参数(最后一个参数名)的类型。因为该参数的地址会在va_start()宏中使用,所以该参数不应被声明为寄存器、函数或数组类型的变量

type va_arg(va_list ap,type)
va_arg()宏获取在本函数参数栈中以type类型获取一个值。ap是经va_start()初始化的va_list ap。每调用va_arg()一次,ap就会被修改指向下一个参数。参数type是一个类型名,以指定获取参数值的方式。第一次在使用va_start()宏后调用va_arg()宏时,将返回last后的一个type类型的参数。后续的调用将一次返回相应类型的参数。如果不再有下一个参数,或者type类型跟下一个参数的实际类型不一致(再以type类型取值一直取下去),将会发生随机的错误。如果ap被传递给一个使用va_arg(ap,type)的函数,那么在该函数返回后,ap的值是不定的。

void va_end(va_list ap)
在同一个函数中,每个va_start()调用都必须对应一个va_end()调用。在调用va_end(ap)后,变量ap的值是不定的。在每个va_start()和va_end()之间多次遍历参数是可能的。va_end()可以是宏或者函数。

[变长参数宏实现 —— 根据参数在栈中的分布得来:《汇编语言》、《程序员的自我修养》]

(3) 矩阵的压缩存储

07.06
如何存储矩阵的元,使矩阵的各种运算能有效地进行。在数值分析中经常出现一些阶数很高的矩阵,同时在矩阵中有许多值相同的元素或者是0元素。有时为了节省存储空间,可以对这类矩阵进行压缩存储。所谓压缩存储是指:为多个值相同的元只分配一个存储空间;对零元不分配空间。假若值相同的元素或者零元素在矩阵中的分布有一定的规律,则我们称此类矩阵为特殊矩阵;反正,称为稀疏矩阵[值相同或零元素分布无规律且这些值占矩阵维数的比例较小,如小于0.05]。

5.2 广义表

(1) 广义表的定义

广义表一般记作

LS=(α1,α2,,αn)

在线性表中, αi 。而在广义表中, αi 可以是单个元素,也可以是广义表,分别称为广义表的 原子子表。当广义表非空时,称第一个元素 α1 为LS的 表头,称其余元素组成的表 (α2,α3,,αn) 是LS的 表尾。[ 结合例子和描述广义表存储结构的结构体理解广义表]

(2) 广义表的存储结构

由于广义表中的数据元素可以具有不同的结构(原子或子表),因此难以用顺序存储结构表示,通常采用链式存储结构,每个数据元素可用一个结点表示。

6 树和二叉树

6.1 树

07.07

结点。组成树的基本元素,如上图树中的A到M都是结点。
。结点拥有的子树数称为结点。
树的度。树内各节点的度的最大值。
叶子(终端结点)。度为0的结点。
孩子、双亲。结点子树的根称为该结点的孩子,该结点称为孩子的双亲。[如A结点下的B、E、K、L结点为一个子树,A结点是B结点的双亲,B结点是A结点的孩子]
兄弟。同一个双亲的孩子之间互称为兄弟。
层次。结点的层次从根开始定义起,根为第一层,根的孩子为第二层。
堂兄弟。若某结点在第l层,则其子树的根就在第l + 1层。其双亲在同一层的结点互为堂兄弟(G与E、F、H、I、J)。
树的深度。树中结点的最大层次称为树的深度或高度。
祖先和子孙。结点的祖先是从根结点到该结点所经分支上的所有结点。以某结点为根的子树中的任一结点都称为该结点的子孙。
有/无序树。如果将树中结点的各子树看成从左至右是有次序的(即不能互换),则称该树为有序树,否则为无序树。在有序树中最左边子树的根称为第一个孩子,最右边的称为最后一个孩子。
森林。森林是m(m >= 0)棵互不相交的树的集合。对树中每个结点而言,其子树的集合即为森林。

6.1 二叉树

(1) 定义 & 性质


二叉树。二叉树的特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。

二叉树性质
[1] 在二叉树的第i层上至多有 2i1 个结点。
[2] 深度为k的二叉树最多有 2k1 个结点。(具n个结点的二叉树的深度为 logn2+1
[3] 对任何一棵二叉树T,如果其终端结点数为 n0 ,度为2的结点数为 n2 ,则 n0=n2+1

满二叉树。一个深度为k且有 2k1 个结点的二叉树称为满二叉树。
完全二叉树。深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的节点一一对应时(自上而下,自左至右),称之为完全二叉树。

完全二叉树的性质
[1] 具有n个结点的完全二叉树的深度为 logn2+1
[2] 如果对一棵有n个结点的完全二叉树的结点按层序编号(从第一层到最后一层,编号从左至右),则对任一结点i(1 <= i <= n),有

  • ·若i = 1,则结点i是二叉树的根;若i > 1,则双亲结点是结点 i/2
  • ·如果2i > n,则结点i无左孩子(结点i为叶子结点);否则(2i < n)其左孩子是结点2i。
  • ·若2i + 1 > n,则结点i无右孩子;否则其右孩子是结点2i + 1。

07.08

(2) 存储结构

(3) 二叉树的遍历

遍历对于线性结构来说,是一件容易的事情。对于像二叉树这样的非线性结构,需要寻找一种规律,以便使二叉树上的结点能排列在一个现行队列上,从而便于遍历
根据二叉树的递归定义和各结点先后顺序(完全二叉树顺序)[好大的跳跃],二叉树的遍历可分为“先序”、“中序”以及“后续”遍历三种。

6.3 赫夫曼树

07.11

(1) 赫夫曼树含义

树中两个结点之间的路径长度。从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径,路径上的分支数目称作路径长度。

树的路径长度。从树根到每一个结点的路径长度之和。
结点的带权路径长度。从该结点到树根之间的路径长度与该结点上(如搜索到该结点的概率)的乘积。
树的带权路径长度。树中所有叶子结点的带权路径长度之和。
赫夫曼树。假设有n个权值 {ω1,ω2,,ωn} ,试构造一棵有n个叶子结点的二叉树,每个叶子结点带权为 ωi ,其中带权路径长度最小的二叉树为最优二叉树赫夫曼树。[对于遍历来说,倘若叶子结点的双亲为某条件,显然将条件发生概率大者的双亲和相应的叶子结点放在离根结点更近的位置有利于减少遍历次数,这是霍夫曼树(应用或思想)的一个体现]

(2) 构造赫夫曼树

根据赫夫曼算法构造赫夫曼树:
[1] 根据给定的n个权值 {ω1,ω2,,ωn} 构成n棵二叉树的集合 F={T1,T2,,Tn} ,其中每棵二叉树 Ti 中只有一个带权为 ωi 的根结点,其左右子树均为空。
[2] 在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
[3] 在F中删除这两棵树,同时将新得到的二叉树加入F中。
[4] 重复[2]和[3],直到F只含一棵树为止。这棵树便是赫夫曼树。

例。

07.12

(3) 赫夫曼编码

6.4 红黑树

[-来自度娘-]

二叉查找树。也称有序二叉树(排序二叉树),是指一棵空树或者具有以下性质的二叉树:[1] 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;[2] 若任意结点的右子树不为空,则右子树上所有结点的值绝大于它的根结点值;[3] 没有键值相等的结点。

平衡二叉树。平衡二叉树具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。(常用算法有红黑树、AVL、Treap、伸展树等)

红黑树。红黑树是一种具有二叉查找树性质的自平衡二叉树:在进行插入和删除操作时通过特定的操作保持二叉树的平衡,从而获得较高的查找性能。[它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并在在实践中是高效的:它可以在 O(logn2) 时间内做查找、插入和删除,n为树中元素的数目。二叉查找树若退化成了一棵具有n个结点的线性链后,则操作的最坏的运行时间为O(n)。红黑树:运行时间 O(n) O(logn2) 的算法(对二叉查找树的限制) —— 具体被优化的时间可参看《编程珠玑》]

红黑树性质

一棵红黑树

[众多网址引用,据说来自维基百科]

树的旋转。左旋:当在某个结点node上,做左旋操作时,假设右孩子不为NULL,则node结点所代表的子树可以作为其右孩子的左结点,原来右节点中的左子结点依照具体情况成为node上的左结点或右结点。(右旋同理)左旋如下图

[图来自http://blog.csdn.net/v_JULY_v/article/details/6105630]
红黑树的插入和删除。红黑树的插入相当于在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作(插入结点着色、旋转,具体不详,先点到为止)。

[2016.07.01 - 0:09]

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设