精华内容
下载资源
问答
  • 数据结构基础概念篇

    万次阅读 多人点赞 2017-11-14 13:44:24
    数据结构一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。数据:所有能被输入到计算机中,且能...

    数据结构

    一些概念

    数据结构就是研究数据的逻辑结构物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。

    1. 数据:所有能被输入到计算机中,且能被计算机处理的符号的集合。是计算机操作的对象的总称。
    2. 数据元素:数据(集合)中的一个“个体”,数据及结构中讨论的基本单位
    3. 数据项:数据的不可分割的最小单位。一个数据元素可由若干个数据项组成。
    4. 数据类型:在一种程序设计语言中,变量所具有的数据种类。整型、浮点型、字符型等等

    5. 逻辑结构:数据之间的相互关系。

      • 集合 结构中的数据元素除了同属于一种类型外,别无其它关系。
      • 线性结构 数据元素之间一对一的关系
      • 树形结构 数据元素之间一对多的关系
      • 图状结构或网状结构 结构中的数据元素之间存在多对多的关系
    6. 物理结构/存储结构:数据在计算机中的表示。物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、哈希结构)等
    7. 在数据结构中,从逻辑上可以将其分为线性结构和非线性结构
    8. 数据结构的基本操作的设置的最重要的准则是,实现应用程序与存储结构的独立。实现应用程序是“逻辑结构”,存储的是“物理结构”。逻辑结构主要是对该结构操作的设定,物理结构是描述数据具体在内存中的存储(如:顺序结构、链式结构、索引结构、希哈结构)等。
    9. 顺序存储结构中,线性表的逻辑顺序和物理顺序总是一致的。但在链式存储结构中,线性表的逻辑顺序和物理顺序一般是不同的。

    10. 算法五个特性: 有穷性、确定性、可行性、输入、输出

    11. 算法设计要求:正确性、可读性、健壮性、高效率与低存储量需求。(好的算法)
    12. 算法的描述有伪程序、流程图、N-S结构图等。E-R图是实体联系模型,不是程序的描述方式。
    13. 设计算法在执行时间时需要考虑:算法选用的规模、问题的规模
    14. 时间复杂度:算法的执行时间与原操作执行次数之和成正比。时间复杂度有小到大:O(1)、O(logn)、O(n)、O(nlogn)、O(n2)、O(n3)。幂次时间复杂度有小到大O(2n)、O(n!)、O(nn)
    15. 空间复杂度:若输入数据所占空间只取决于问题本身,和算法无关,则只需要分析除输入和程序之外的辅助变量所占额外空间

    线性表

    线性表是一种典型的线性结构。头结点无前驱有一个后继,尾节点无后继有一个前驱。链表只能顺序查找,定位一个元素的时间为O(N),删除一个元素的时间为O(1)

    1. 线性表的顺序存储结构:把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表。是一种随机存取的存储结构。顺序存储指内存地址是一块的,随机存取指访问时可以按下标随机访问,存储和存取是不一样的。如果是存储,则是指按顺序的,如果是存取,则是可以随机的,可以利用元素下标进行。数组比线性表速度更快的是:原地逆序、返回中间节点、选择随机节点。
      • 便于线性表的构造和任意元素的访问
      • 插入:插入新结点,之后结点后移。平均时间复杂度:O(n)
      • 删除:删除节点,之后结点前移。平均时间复杂度:O(n)
    2. 线性链表:用一组任意的存储单元来依次存放线性表的结点,这组存储单元即可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址。data域是数据域,用来存放结点的值。next是指针域(亦称链域),用来存放结点的直接后继的地址(或位置)。不需要事先估计存储空间大小。
      • 单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。同时,由于最后一个结点无后继,故结点的指针域为空,即NULL。头插法建表(逆序)、尾插法建表(顺序)。增加头结点的目的是算法实现上的方便,但增大了内存开销。
        • 查找:只能从链表的头指针出发,顺链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构
        • 插入:先找到表的第i-1的存储位置,然后插入。新结点先连后继,再连前驱。
        • 删除:首先找到ai-1的存储位置p。然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间.r=p->next;p->next=r->next;delete r。
        • 判断一个单向链表中是否存在环的最佳方法是快慢指针。
      • 静态链表:用一维数组来实现线性链表,这种用一维数组表示的线性链表,称为静态链表。静态:体现在表的容量是一定的。(数组的大小);链表:插入与删除同前面所述的动态链表方法相同。静态链表中指针表示的是下一元素在数组中的位置。
      • 静态链表是用数组实现的,是顺序的存储结构,在物理地址上是连续的,而且需要预先分配大小。动态链表是用申请内存函数(C是malloc,C++是new)动态申请内存的,所以在链表的长度上没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,要通过指针来顺序访问。静态链表在插入、删除时也是通过修改指针域来实现的,与动态链表没有什么分别
      • 循环链表:是一种头尾相接的链表。其特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
        • 在单链表中,将终端结点的指针域NULL改为指向表头结点的或开始结点,就得到了单链形式的循环链表,并简单称为单循环链表。由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p—>next是否为空,而是判断它们是否等于某一指定指针,如头指针或尾指针等。
      • 双向链表:在单链表的每个结点里再增加一个指向其直接前趋的指针域prior。这样就形成的链表中有两个方向不同的链。双链表一般由头指针唯一确定的,将头结点和尾结点链接起来构成循环链表,并称之为双向链表。设指针p指向某一结点,则双向链表结构的对称性可用下式描述:p—>prior—>next=p=p—>next—>prior。从两个方向搜索双链表,比从一个方向搜索双链表的方差要小。
        • 插入:先搞定插入节点的前驱和后继,再搞定后结点的前驱,最后搞定前结点的后继。
        • 在有序双向链表中定位删除一个元素的平均时间复杂度为O(n)
        • 可以直接删除当前指针所指向的节点。而不需要像单向链表中,删除一个元素必须找到其前驱。因此在插入数据时,单向链表和双向链表操作复杂度相同,而删除数据时,双向链表的性能优于单向链表

    栈和队列

    栈(Stack)是限制在表的一端进行插入和删除运算的线性表,通常称插入、删除的这一端为栈顶(Top),另一端为栈底(Bottom)。先进后出。top= -1时为空栈,top=0只能说明栈中只有一个元素,并且元素进栈时top应该自增

    1. 顺序存储栈:顺序存储结构
    2. 链栈:链式存储结构。插入和删除操作仅限制在链头位置上进行。栈顶指针就是链表的头指针。通常不会出现栈满的情况。 不需要判断栈满但需要判断栈空。
    3. 两个栈共用静态存储空间,对头使用也存在空间溢出问题。栈1的底在v[1],栈2的底在V[m],则栈满的条件是top[1]+1=top[2]。
    4. 基本操作:删除栈顶元素、判断栈是否为空以及将栈置为空栈等
    5. 对于n各元素的入栈问题,可能的出栈顺序有C(2n,n)/(n+1)个。
    6. 堆栈溢出一般是循环的递归调用、大数据结构的局部变量导致的

    应用,代码

    1. 进制转换
    2. 括号匹配的检验
    3. 行编辑程序
    4. 迷宫求解:若当前位置“可通”,则纳入路径,继续前进;若当前位置“不可通”,则后退,换方向继续探索;若四周“均无通路”,则将当前位置从路径中删除出去。
    5. 表达式求解:前缀、中缀、后缀。
      • 操作数之间的相对次序不变;
      • 运算符的相对次序不同;
      • 中缀式丢失了括弧信息,致使运算的次序不确定
      • 前缀式的运算规则为:连续出现的两个操作数和在它们之前且紧靠它们的运算符构成一个最小表达式
      • 后缀式的运算规则为:运算符在式中出现的顺序恰为表达式的运算顺序;每个运算符和在它之前出现且紧靠它的两个操作数构成一个最小表达式。
    6. 实现递归:多个函数嵌套调用的规则是:后调用先返回。
    7. 浏览器历史纪录,Android中的最近任务,Activity的启动模式,CPU中栈的实现,Word自动保存,解析计算式,解析xml/json。解析XML时,需要校验节点是否闭合,节点闭合的话,有头尾符号相对应,遇到头符号将其放入栈中,遇到尾符号时,弹出栈的内容,看是否有与之对应的头符号,栈的特性刚好符合符号匹配的就近原则。

    不是所有的递归程序都需要栈来保护现场,比方说求阶乘的,是单向递归,直接用循环去替代从1乘到n就是结果了,另外一些需要栈保存的也可以用队列等来替代。不是所有的递归转化为非递归都要用到栈。转化为非递归主要有两种方法:对于尾递归或单向递归,可以用循环结构算法代替

    队列

    队列(Queue)也是一种运算受限的线性表。它只允许在表的一端进行插入,而在另一端进行删除。允许删除的一端称为队头(front),允许插入的一端称为队尾(rear)。先进先出。

    1. 顺序队列:顺序存储结构。当头尾指针相等时队列为空。在非空队列里,头指针始终指向队头前一个位置,而尾指针始终指向队尾元素的实际位置
    2. 循环队列。在循环队列中进行出队、入队操作时,头尾指针仍要加1,朝前移动。只不过当头尾指针指向向量上界(MaxSize-1)时,其加1操作的结果是指向向量的下界0。除非向量空间真的被队列元素全部占用,否则不会上溢。因此,除一些简单的应用外,真正实用的顺序队列是循环队列。故队空和队满时头尾指针均相等。因此,我们无法通过front=rear来判断队列“空”还是“满”
    3. 链队列:链式存储结构。限制仅在表头删除和表尾插入的单链表。显然仅有单链表的头指针不便于在表尾做插入操作,为此再增加一个尾指针,指向链表的最后一个结点。
    4. 设尾指针的循环链表表示队列,则入队和出队算法的时间复杂度均为O(1)。用循环链表表示队列,必定有链表的头结点,入队操作在链表尾插入,直接插入在尾指针指向的节点后面,时间复杂度是常数级的;出队操作在链表表头进行,也就是删除表头指向的节点,时间复杂度也是常数级的。

    5. 队空条件:rear==front,但是一般需要引入新的标记来说明栈满还是栈空,比如每个位置布尔值

    6. 队满条件:(rear+1) % QueueSize==front,其中QueueSize为循环队列的最大长度
    7. 计算队列长度:(rear-front+QueueSize)% QueueSize
    8. 入队:(rear+1)% QueueSize
    9. 出队:(front+1)% QueueSize
    10. 假设以数组A[N]为容量存放循环队列的元素,其头指针是front,当前队列有X个元素,则队列的尾指针值为(front+X mod N)

    串(String)是零个或多个字符组成的有限序列。长度为零的串称为空串(Empty String),它不包含任何字符。通常将仅由一个或多个空格组成的串称为空白串(Blank String) 注意:空串和空白串的不同,例如“ ”和“”分别表示长度为1的空白串和长度为0的空串。

    串的表示和实现:

    1. 定长顺序存储表示。静态存储分配的顺序表。
    2. 堆分配存储表示。存储空间是在程序执行过程中动态分配而得。所以也称为动态存储分配的顺序表
    3. 串的链式存储结构。

    串匹配:将主串称为目标串,子串称之为模式串。蛮力法匹配。KMP算法匹配。Boyer-Moore算法匹配。

    数组和广义表

    数组和广义表可看成是一种特殊的线性表,其特殊在于: 表中的元素本身也是一种线性表。内存连续。根据下标在O(1)时间读/写任何元素。

    二维数组,多维数组,广义表、树、图都属于非线性结构

    数组

    数组的顺序存储:行优先顺序;列优先顺序。数组中的任一元素可以在相同的时间内存取,即顺序存储的数组是一个随机存取结构。

    关联数组(Associative Array),又称映射(Map)、字典( Dictionary)是一个抽象的数据结构,它包含着类似于(键,值)的有序对。 不是线性表。

    矩阵的压缩:

    1. 对称矩阵、三角矩阵:直接存储矩阵的上三角或者下三角元素。注意区分i>=j和i

    广义表

    广义表(Lists,又称列表)是线性表的推广。广义表是n(n≥0)个元素a1,a2,a3,…,an的有限序列,其中ai或者是原子项,或者是一个广义表。若广义表LS(n>=1)非空,则a1是LS的表头,其余元素组成的表(a2,…an)称为LS的表尾。广义表的元素可以是广义表,也可以是原子,广义表的元素也可以为空。表尾是指除去表头后剩下的元素组成的表,表头可以为表或单元素值。所以表尾不可以是单个元素值。

    例子:

    1. A=()——A是一个空表,其长度为零。
    2. B=(e)——表B只有一个原子e,B的长度为1。
    3. C=(a,(b,c,d))——表C的长度为2,两个元素分别为原子a和子表(b,c,d)。
    4. D=(A,B,C)——表D的长度为3,三个元素都是广义 表。显然,将子表的值代入后,则有D=(( ),(e),(a,(b,c,d)))。
    5. E=(a,E)——这是一个递归的表,它的长度为2,E相当于一个无限的广义表E=(a,(a,(a,(a,…)))).

    三个结论:

    1. 广义表的元素可以是子表,而子表的元素还可以是子表。由此,广义表是一个多层次的结构,可以用图形象地表示
    2. 广义表可为其它表所共享。例如在上述例4中,广义表A,B,C为D的子表,则在D中可以不必列出子表的值,而是通过子表的名称来引用。
    3. 广义表的递归性

    考点:

    1. 广义表是0个或多个单因素或子表组成的有限序列,广义表可以是自身的子表,广义表的长度n>=0,所以可以为空表。广义表的同级元素(直属于同一个表中的各元素)具有线性关系
    2. 广义表的表头为空,并不代表该广义表为空表。广义表()和(())不同。前者是长度为0的空表,对其不能做求表头和表尾的运算;而后者是长度为l的非空表(只不过该表中惟一的一个元素是空表),对其可进行分解,得到的表头和表尾均是空表()
    3. 已知广义表LS=((a,b,c),(d,e,f)),运用head和tail函数取出LS中原子e的运算是head(tail(head(tail(LS)))。根据表头、表尾的定义可知:任何一个非空广义表的表头是表中第一个元素,它可以是原子,也可以是子表,而其表尾必定是子表。也就是说,广义表的head操作,取出的元素是什么,那么结果就是什么。但是tail操作取出的元素外必须加一个表——“()“。tail(LS)=((d,e,f));head(tail(LS))=(d,e,f);tail(head(tail(LS)))=(e,f);head(tail(head(tail(LS))))=e。
    4. 二维以上的数组其实是一种特殊的广义表
    5. 在(非空)广义表中:1、表头head可以是原子或者一个表 2、表尾tail一定是一个表 3.广义表难以用顺序存储结构 4.广义表可以是一个多层次的结构

    树和二叉树

    一种非线性结构。树是递归结构,在树的定义中又用到了树的概念。

    基本术语:

    1. 树结点:包含一个数据元素及若干指向子树的分支;
    2. 孩子结点:结点的子树的根称为该结点的孩子;
    3. 双亲结点:B结点是A结点的孩子,则A结点是B结点的双亲;
    4. 兄弟结点:同一双亲的孩子结点;
    5. 堂兄结点:同一层上结点;
    6. 结点层次:根结点的层定义为1;根的孩子为第二层结点,依此类推;
    7. 树的高(深)度:树中最大的结点层
    8. 结点的度:结点子树的个数
    9. 树的度: 树中最大的结点度。
    10. 叶子结点:也叫终端结点,是度为0的结点;
    11. 分枝结点:度不为0的结点(非终端结点);
    12. 森林:互不相交的树集合;
    13. 有序树:子树有序的树,如:家族树;
    14. 无序树:不考虑子树的顺序;

    二叉树

    二叉树可以为空。二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也要进行区分,说明它是左子树,还是右子树。这是二叉树与树的最主要的差别。注意区分:二叉树、二叉查找树/二叉排序树/二叉搜索树二叉平衡(查找)树

    二叉平衡树肯定是一颗二叉排序树。堆不是一颗二叉平衡树。

    二叉树与树是不同的,二叉树不等价于分支树最多为二的有序树。当一个结点只包含一个子节点时,对于有序树并无左右孩子之分,而对于二叉树来说依然有左右孩子之分,所以二叉树与树是两种不同的结构。

    性质:

    1. 在二叉树的第 i 层上至多有2i-1个结点。
    2. 深度为 k 的二叉树上至多含 2k-1 个结点(k≥1)
    3. 对任何一棵二叉树,若它含有n0个叶子结点、n2个度为 2 的结点,则必存在关系式:n0= n2+1。
    4. 具有 n 个结点的完全二叉树的深度为⎣log2 n⎦+1 。
    5. n个结点的二叉树中,完全二叉树具有最小的路径长度。
    6. 如果对一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i(1<=i<=n),有:
      • 如果i=1,则结点i无双亲,是二叉树的根;如果i>1,则其双亲的编号是 i/2(整除)。
      • 如果2i>n,无左孩子;否则,其左孩子是结点2i。
      • 如果2i+1>n,则结点i无右孩子;否则,其右孩子是结点2i+1。

    二叉树的存储结构

    1. 顺序存储结构:仅仅适用于满或完全二叉树,结点之间的层次关系由性质5确定。
    2. 二叉链表法:每个节点存储左子树和右子树。三叉链表:左子树、右子树、父节点,总的指针是n+2
    3. 在有n个结点的二叉链表中,值为非空的链域的个数为n-1。在有N个结点的二叉链表中必定有2N个链域。除根结点外,其余N-1个结点都有一个父结点。所以,一共有N-1个非空链域,其余2N-(N-1)=N+1个为空链域。
    4. 二叉链存储法也叫孩子兄弟法,左指针指向左孩子,右指针指向右兄弟。而中序遍历的顺序是左孩子,根,右孩子。这种遍历顺序与存储结构不同,因此需要堆栈保存中间结果。而中序遍历检索二叉树时,由于其存储结构跟遍历顺序相符,因此不需要用堆栈。

    遍历二叉树和线索二叉树

    遍历二叉树:使得每一个结点均被访问一次,而且仅被访问一次。非递归的遍历实现要利用栈。

    • 先序遍历DLR:根节点->左子树->右子树
    • 中序遍历LDR:左子树->根节点->右子树。必须要有中序遍历才能得到一棵二叉树的正确顺序
    • 后续遍历LRD:左子树->右子树->根节点。需要栈的支持。
    • 层次遍历:用一维数组存储二叉树时,总是以层次遍历的顺序存储结点。层次遍历应该借助队列。

    线索二叉树:对二叉树所有结点做某种处理可在遍历过程中实现;检索(查找)二叉树某个结点,可通过遍历实现;如果能将二叉树线索化,就可以简化遍历算法,提高遍历速度,目的是加快查找结点的前驱或后继的速度。

    如何线索化?以中序遍历为例,若能将中序序列中每个结点前趋、后继信息保存起来,以后再遍历二叉树时就可以根据所保存的结点前趋、后继信息对二叉树进行遍历。对于二叉树的线索化,实质上就是遍历一次二叉树,只是在遍历的过程中,检查当前结点左,右指针域是否为空,若为空,将它们改为指向前驱结点或后继结点的线索。前驱就是在这一点之前走过的点,不是下一将要去往的点

    加上结点前趋后继信息(结索)的二叉树称为线索二叉树。n个结点的线索二叉树上每个结点有2个指针域(指向左孩子和右孩子),总共有2n个指针域;一个n个结点的树有n-1条边,那么空指针域= 2n - (n-1) = n + 1,即线索数为n+1。指针域tag为0,存放孩子指针,为1,存放前驱/后继节点指针。

    线索树下结点x的前驱与后继查找:设结点x相应的左(右)标志是线索标志,则lchild(rchild)就是前驱(后继),否则:

    • LDR–前驱:左子树中最靠右边的结点;后继:右子树中最靠左边的结点
    • LRD–前驱:右子树的根,若无右子树,为左子树跟。后继:x是根,后继是空;x是双亲的右孩子、x是双亲的左孩子,但双亲无右孩子,双亲是后继;x是双亲的左孩子,双亲有右孩子,双亲右子树中最左的叶子是后继
    • DLR–对称于LRD线索树—将LRD中所有左右互换,前驱与后继互换,得到DLR的方法。
    • 为简化线索链表的遍历算法,仿照线性链表,为线索链表加上一头结点,约定:
      • 头结点的lchild域:存放线索链表的根结点指针;
      • 头结点的rchild域: 中序序列最后一个结点的指针;
      • 中序序列第一结点lchild域指向头结点;
      • 中序序列最后一个结点的rchild域指向头结点;

    中序遍历的线索二叉树以及线索二叉树链表示意图
    xiansuobinarytree

    一棵左右子树均不空的二叉树在前序线索化后,其中空的链域的个数是1。前序和后续线索化后空链域个数都是1,中序是2。二叉树在线索化后,仍不能有效求解的问题是前序求前序先驱,后序求后序后继。

    中序遍历的顺序为:左、根、右,所以对于每一非空的线索,左子树结点的后继为根结点,右子树结点的前驱为根结点,再递归的执行上面的过程,可得非空线索均指向其祖先结点。在中序线索二叉树中,每一非空的线索均指向其祖先结点

    在二叉树上加上结点前趋、后继线索后,可利用线索对二叉树进行遍历,此时,不需栈,也不需递归。基本步骤:

    1. p=T->lchild; p指向线索链表的根结点;
    2. 若线索链表非空,循环:
      • 循环,顺着p左孩子指针找到最左下结点;访问之;
      • 若p所指结点的右孩子域为线索,p的右孩子结点即为后继结点循环: p=p->rchild; 并访问p所指结点;(在此循环中,顺着后继线索访问二叉树中的结点)
      • 一旦线索“中断”,p所指结点的右孩子域为右孩子指针,p=p->rchild,使 p指向右孩子结点;

    树和森林

    树的存储结构:

    1. 双亲表示法
    2. 孩子表示法
    3. 利用图表示树
    4. 孩子兄弟表示法(二叉树表示法):链表中每个结点的两指针域分别指向其第一个孩子结点和下一个兄弟结点

    将树转化成二叉树:右子树一定为空

    1. 加线:在兄弟之间加一连线
    2. 抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
    3. 旋转:以树的根结点为轴心,将整树顺时针转45°

    森林转换成二叉树:

    1. 将各棵树分别转换成二叉树
    2. 将每棵树的根结点用线相连
    3. 以第一棵树根结点为二叉树的根

    树与转换后的二叉树的关系:转换后的二叉树的先序对应树的先序遍历;转换后的二叉树的中序对应树的后序遍历

    哈弗曼树/霍夫曼树

    一些概念

    1. 路径:从一个祖先结点到子孙结点之间的分支构成这两个结点间的路径;
    2. 路径长度:路径上的分支数目称为路径长度;
    3. 树的路径长度:从根到每个结点的路径长度之和。
    4. 结点的权:根据应用的需要可以给树的结点赋权值;
    5. 结点的带权路径长度:从根到该结点的路径长度与该结点权的乘积;
    6. 树的带权路径长度=树中所有叶子结点的带权路径之和;通常记作 WPL=∑wi×li
    7. 哈夫曼树:假设有n个权值(w1, w2, … , wn),构造有n个叶子结点的二叉树,每个叶子结点有一个 wi作为它的权值。则带权路径长度最小的二叉树称为哈夫曼树。最优二叉树。

    前缀码的定义:在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀。霍夫曼编码就是前缀码,可用于快速判断霍夫曼编码是否正确。霍夫曼树是满二叉树,若有n个节点,则共有(n+1)/2个码子

    给定n个权值作为n的叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树(Huffman Tree)。霍夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

    假设哈夫曼树是二叉的话,则度为0的结点个数为N,度为2的结点个数为N-1,则结点总数为2N-1。哈夫曼树的结点个数必为奇数。

    哈夫曼树不一定是完全二叉树,但一定是最优二叉树。

    若度为m的哈夫曼树中,其叶结点个数为n,则非叶结点的个数为[(n-1)/(m-1)]。边的数目等于度。

    图遍历与回溯

    图搜索->形成搜索树

    1. 穷举法。
    2. 贪心法。多步决策,每步选择使得构成一个问题的可能解,同时满足目标函数。
    3. 回溯法。根据题意,选取度量标准,然后将可能的选择方法按度量标准所要求顺序排好,每次处理一个量,得到该意义下的最优解的分解处理。

    无向图

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路
    3. 连通:顶点v至v’ 之间有路径存在
    4. 连通图:无向图图 G 的任意两点之间都是连通的,则称G是连通图。
    5. 连通分量:极大连通子图,子图中包含的顶点个数极大
    6. 所有顶点度的和必须为偶数

    有向图:

    1. 回路或环:第一个顶点和最后一个顶点相同的路径。
    2. 简单回路或简单环:除第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路。
    3. 连通:顶点v至v’之间有路径存在
    4. 强连通图:有向图G的任意两点之间都是连通的,则称G是强连通图。各个顶点间均可达。
    5. 强连通分量:极大连通子图
    6. 有向图顶点的度是顶点的入度与出度之和。邻接矩阵中第V行中的1的个数是V的出度

    7. 生成树:极小连通子图。包含图的所有n个结点,但只含图的n-1条边。在生成树中添加一条边之后,必定会形成回路或环。

    8. 完全图:有 n(n-1)/2 条边的无向图。其中n是结点个数。必定是连通图。
    9. 有向完全图:有n(n-1)条边的有向图。其中n是结点个数。每两个顶点之间都有两条方向相反的边连接的图。
    10. 一个无向图 G=(V,E) 是连通的,那么边的数目大于等于顶点的数目减一:|E|>=|V|-1,而反之不成立。如果 G=(V,E) 是有向图,那么它是强连通图的必要条件是边的数目大于等于顶点的数目:|E|>=|V|,而反之不成立。没有回路的无向图是连通的当且仅当它是树,即等价于:|E|=|V|-1。

    图的存储形式

    1. 邻接矩阵和加权邻接矩阵
      • 无权有向图:出度: i行之和;入度: j列之和。
      • 无权无向图:i结点的度: i行或i列之和。
      • 加权邻接矩阵:相连为w,不相连为∞
    2. 邻接表
      • 用顶点数组表、边(弧)表表示该有向图或无向图
      • 顶点数组表:用数组存放所有的顶点。数组大小为图顶点数n
      • 边表(边结点表):每条边用一个结点进行表示。同一个结点的所有的边形成它的边结点单链表。
      • n个顶点的无向图的邻接表最多有n(n-1)个边表结点。有n个顶点的无向图最多有n*(n-1)/2条边,此时为完全无向图,而在邻接表中每条边存储两次,所以有n*(n-1)个结点

    图的遍历

    深度优先搜索利用栈,广度优先搜索利用队列

    求一条从顶点i到顶点s的简单路径–深搜。求两个顶点之间的一条长度最短的路径–广搜。当各边上的权值均相等时,BFS算法可用来解决单源最短路径问题。

    生成树和最小生成树

    每次遍历一个连通图将图的边分成遍历所经过的边和没有经过的边两部分,将遍历经过的边同图的顶点构成一个子图,该子图称为生成树。因此有DFS生成树和BFS生成树。

    生成树是连通图的极小子图,有n个顶点的连通图的生成树必定有n-1条边,在生成树中任意增加一条边,必定产生回路。若砍去它的一条边,就会把生成树变成非连通子图

    最小生成树:生成树中边的权值(代价)之和最小的树。最小生成树问题是构造连通网的最小代价生成树。

    Kruskal算法:令最小生成树集合T初始状态为空,在有n个顶点的图中选取代价最小的边并从图中删去。若该边加到T中有回路则丢弃,否则留在T中;依此类推,直至T中有n-1条边为止。

    Prim算法、Kruskal算法和Dijkstra算法均属于贪心算法。

    1. Dijkstra算法解决的是带权重的有向图上单源最短路径问题,该算法要求所有边的权重都为非负值。
    2. Dijkstra算法解决了从某个原点到其余各顶点的最短路径问题,由循环嵌套可知该算法的时间复杂度为O(N*N)。若要求任一顶点到其余所有顶点的最短路径,一个比较简单的方法是对每个顶点当做源点运行一次该算法,等于在原有算法的基础上,再来一次循环,此时整个算法的复杂度就变成了O(N*N*N)。
    3. Bellman-Ford算法解决的是一般情况下的单源最短路径问题,在这里,边的权重可以为负值。该算法返回一个布尔值,以表明是否存在一个从源节点可以到达的权重为负值的环路。如果存在这样一个环路,算法将告诉我们不存在解决方案。如果没有这种环路存在,算法将给出最短路径和它们的权重。

    双连通图和关节点

    若从一个连通图中删去任何一个顶点及其相关联的边,它仍为一个连通图的话,则该连通图被称为重(双)连通图

    若连通图中的某个顶点和其相关联的边被删去之后,该连通图被分割成两个或两个以上的连通分量,则称此顶点为关节点

    没有关节点的连通图为双连通图

    1. 若生成树的根结点,有两个或两个以上的分支,则此顶点(生成树的根)必为关节点;
    2. 对生成树上的任意一个非叶“顶点”,若其某棵子树中的所有“顶点”没有和其祖先相通的回边,则该“顶点”必为关节点。

    有向无环图及其应用

    拓扑排序。在用邻接表表示图时,对有n个顶点和e条弧的有向图而言时间复杂度为O(n+e)。一个有向图能被拓扑排序的充要条件就是它是一个有向无环图。拓扑序列唯一不能唯一确定有向图。

    AOV网(Activity On Vertex):用顶点表示活动,边表示活动的优先关系的有向图称为AOV网。AOV网中不允许有回路,这意味着某项活动以自己为先决条件。

    拓扑有序序列:把AOV网络中各顶点按照它们相互之间的优先关系排列一个线性序列的过程。若vi是vj前驱,则vi一定在vj之前;对于没有优先关系的点,顺序任意。

    拓扑排序:对AOV网络中顶点构造拓扑有序序列的过程。方法:

    1. 在有向图中选一个没有前驱的顶点且输出之
    2. 从图中删除该顶点和所有以它为尾的弧
    3. 重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止(此时说明图中有环)

    采用深度优先搜索拓扑排序算法可以判断出一个有向图中是否有环(回路).深度优先搜索只要在其中记录下搜索的节点数n,当n大于图中节点数时退出,并可以得出有回路。若有回路,则拓扑排序访问不到图中所有的节点,所以也可以得出回路。广度优先搜索过程中如果访问到一个已经访问过的节点,可能是多个节点指向这个节点,不一定是存在环。

    算法描述:

    1. 把邻接表中入度为0的顶点依此进栈
    2. 若栈不空,则
      • 栈顶元素vj退栈并输出;
      • 在邻接表中查找vj的直接后继vk,把vk的入度减1;若vk的入度为0则进栈
    3. 若栈空时输出的顶点个数不是n,则有向图有环;否则,拓扑排序完毕。

    AOE网:带权的有向无环图,其中顶点表示事件,弧表示活动,权表示活动持续时间。在工程上常用来表示工程进度计划。

    一些定义:

    1. 事件的最早发生时间(ve(j)):从源点到j结点的最长的路径。意味着事件最早能够发生的时间。
    2. 事件的最迟发生时间(vl(j)):不影响工程的如期完工,事件j必须发生的时间。
    3. 活动ai由弧

    查找

    顺序查找、折半查找、索引查找、分块查找是静态查找,动态查找有二叉排序树查找,最优二叉树查找,键树查找,哈希表查找

    静态查找表

    顺序表的顺序查找:应用范围:顺序表或线性链表表示的表,表内元素之间无序。查找过程:从表的一端开始逐个进行记录的关键字和给定值的比较。

    顺序有序表的二分查找。平均查找时间(n+1)/n log2(n+1)

    分块查找:将表分成几块,块内无序,块间有序,即前一块中的最大值小于后一块中的最小值。并且有一张索引表,每一项存放每一块的最大值和指向该块第一个元素的指针。索引表有序,块内无序。所以,块间查找用二分查找,块内用顺序查找,效率介于顺序和二分之间;先确定待查记录所在块,再在块内查找。因此跟表中元素个数和块中元素个数都有关。

    1. 用数组存放待查记录,
    2. 建立索引表,由每块中最大(小)的关键字及所属块位置的信息组成。
    3. 当索引表较大时,可以采用二分查找
    4. 在数据量极大时,索引可能很多,可考虑建立索引表的索引,即二级索引,原则上索引不超过三级

    分块查找平均查找长度:ASLbs = Lb + Lw。其中,Lb是查找索引表确定所在块的平均查找长度, Lw是在块中查找元素的平均查找长度。在n一定时,可以通过选择s使ASL尽可能小。当s=sqrt(n)时,ASL最小。

    1. 时间:顺序查找最差,二分最好,分块介于两者之间
    2. 空间:分块最大,需要增加索引数据的空间
    3. 顺序查找对表没有特殊要求
    4. 分块时数据块之间在物理上可不连续。所以可以达到插入、删除数据只涉及对应的块;另外,增加了索引的维护。
    5. 二分查找要求表有序,所以若表的元素的插入与删除很频繁,维持表有序的工作量极大。
    6. 在表不大时,一般直接使用顺序查找。

    动态查找

    二叉排序树的结点删除:

    1. x为叶子结点,则直接删除
    2. x只有左子树xL或只有右子树xR ,则令xL或xR直接成为双亲结点f的子树;
    3. x即有左子树xL也有右子树xR,在xL中选值最大的代替x,该数据按二叉排序树的性质应在最右边。

    平衡二叉树:每个结点的平衡因子都为 1、-1、0 的二叉排序树。或者说每个结点的左右子树的高度最多差1的二叉排序树。

    平衡二叉树的平衡:

    1. 左调整(新结点插入在左子树上的调整):
      • LL(插入在结点左子树的左子树上):旋转前后高度都为h+1
      • LR(新插入结点在左子树的右子树上):旋转前后高度仍为h+1
    2. 右调整(新结点插入在右子树上进行的调整):
      • RR(插入在的右子树的右子树上):处理方法和 LL对称
      • RL(插入在的右子树的左子树上):处理方法和 LR对称

    平衡树建立方法:

    1. 按二叉排序树插入结点
    2. 如引起结点平衡因子变为|2|,则确定旋转点,该点是离根最远(或最接近于叶子的点)
    3. 确定平衡类型后进行平衡处理,平衡后以平衡点为根的子树高不变
    4. 最小二叉平衡树的节点的公式如下 F(n)=F(n-1)+F(n-2)+1 这个类似于一个递归的数列,可以参考Fibonacci数列,1是根节点,F(n-1)是左子树的节点数量,F(n-2)是右子树的节点数量。

    常见的平衡二叉树:

    1. 红黑树是平衡二叉树,也就是左右子树是平衡的,高度大概相等。这种情况等价于一块完全二叉树的高度,查找的时间复杂度是树的高度,为logn,插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)
      红黑树
      • 节点是红色或黑色。
      • 根是黑色。
      • 所有叶子都是黑色(叶子是NIL节点)。
      • 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
      • 从任一节点到其每个叶子的所有简单路径 都包含相同数目的黑色节点。
    2. avl树也是自平衡二叉树;红黑树和AVL树查找、插入、删除的时间复杂度相同;包含n个内部结点的红黑树的高度是o(logn); TreeMap 是一个红黑树的实现,能保证插入的值保证排序
    3. STL和linux多使用红黑树作为平衡树的实现:
      1. 如果插入一个node引起了树的不平衡,AVL和RB-Tree都是最多只需要2次旋转操作,即两者都是O(1);但是在删除node引起树的不平衡时,最坏情况下,AVL需要维护从被删node到root这条路径上所有node的平衡性,因此需要旋转的量级O(logN),而RB-Tree最多只需3次旋转,只需要O(1)的复杂度。
      2. 其次,AVL的结构相较RB-Tree来说更为平衡,在插入和删除node更容易引起Tree的unbalance,因此在大量数据需要插入或者删除时,AVL需要rebalance的频率会更高。因此,RB-Tree在需要大量插入和删除node的场景下,效率更高。自然,由于AVL高度平衡,因此AVL的search效率更高。
      3. map的实现只是折衷了两者在search、insert以及delete下的效率。总体来说,RB-tree的统计性能是高于AVL的。

    查找总结

    1. 既希望较快的查找又便于线性表动态变化的查找方法是哈希法查找。二叉排序树查找,最优二叉树查找,键树查找,哈希法查找是动态查找。分块、顺序、折半、索引顺序查找均为静态。分块法应该是将整个线性表分成若干块进行保存,若动态变化则可以添加在表的尾部(非顺序结构),时间复杂度是O(1),查找复杂度为O(n);若每个表内部为顺序结构,则可用二分法将查找时间复杂度降至O(logn),但同时动态变化复杂度则变成O(n);顺序法是挨个查找,这种方法最容易实现,不过查找时间复杂度都是O(n),动态变化时可将保存值放入线性表尾部,则时间复杂度为O(1);二分法是基于顺序表的一种查找方式,时间复杂度为O(logn);通过哈希函数将值转化成存放该值的目标地址,O(1)
    2. 二叉树的平均查找长度为O(log2n)——O(n).二叉排序树的查找效率与二叉树的高度有关,高度越低,查找效率越高。二叉树的查找成功的平均查找长度ASL不超过二叉树的高度。二叉树的高度与二叉树的形态有关,n个节点的完全二叉树高度最小,高度为[log2n]+1,n个节点的单只二叉树的高度最大,高度为n,此时查找成功的ASL为最大(n+1)/2,因此二叉树的高度范围为[log2n]+1——n.
    3. 链式存储不能随机访问,必须是顺序存储

    B_树的B+树

    B_树

    B-树就是B树。m阶B_树满足或空,或为满足下列性质的m叉树:

    B-树

    1. 树中每个结点最多有m棵子树
    2. 根结点在不是叶子时,至少有两棵子树
    3. 除根外,所有非终端结点至少有⎡m/2⎤棵子树
    4. 有s个子树的非叶结点具有 n = s-1个关键字,结点的信息组织为:(n,A0,K1,A1,K2,A2 … Kn,An)。这里:n为关键字的个数,ki(i=1,2,…,n)为关键字,且满足Ki小于Ki+1,,Ai(i=0,1,..n)为指向子树的指针。
    5. 所有的叶子结点都出现在同一层上,不带信息(可认为外部结点或失败结点)。
    6. 关键字集合分布在整颗树中
    7. 任何一个关键字出现且只出现在一个结点中
    8. 搜索有可能在非叶子结点结束
    9. 其搜索性能等价于在关键字全集内做一次二分查找
    10. 只适用于随机检索,不适用于顺序检索。
    11. 有结点的平衡因子都为零
    12. M阶B-树中含有N个关键字,最大深度为log⎡m/2⎤(n+1)/2+2

    B_树中结点的插入

    1. m代表B_树的阶,插入总发生在最低层
    2. 插入后关键字个数小于等于 m-1,完成。
    3. 插入后关键字个数等于m,结点分裂,以中点数据为界一分为二,中点数据放到双亲结点中。这样就有可能使得双亲结点的数据个数为m,引起双亲结点的分裂,最坏情况下一直波及到根,引起根的分裂——B_树长高。

    3阶B_树的插入。每个结点最多3棵子树,2个数据;最少2棵子树,1个数据。所以3阶B_树也称为2-3树。

    B_树中结点的删除

    1. 删除发生在最底层
      • 被删关键字所在结点中的关键字数目大于等于 m/2 ,直接删除。
      • 删除后结点中数据为⎡m/2⎤-2,而相邻的左(右)兄弟中数据大于⎡m/2⎤-1,此时左(右兄弟)中最大(小)的数据上移到双亲中,双亲中接(靠)在它后(前)面的数据移到被删数据的结点中
      • 其左右兄弟结点中数据都是⎡m/2⎤-1,此时和左(右)兄弟合并,合并时连同双亲中相关的关键字。此时,双亲中少了一项,因此又可能引起双亲的合并,最坏一直到根,使B-树降低一层。
    2. 删除不在最底层
      • 在大于被删数据中选最小的代替被删数据,问题转换成在最底层的删除

    B+树

    在实际的文件系统中,用的是B+树或其变形。有关性质与操作类似与B_树。

    B+树

    差异:

    1. 有n棵子树的结点中有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。
    2. 所有叶子结点中包含全部关键字信息,及对应记录位置信息及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。(而B树的叶子节点并没有包括全部需要查找的信息)
    3. 所有非叶子为索引,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B树的非终节点也包含需要查找的有效信息)
    4. 非叶最底层顺序联结,这样可以进行顺序查找

    B+特性

    1. 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
    2. 不可能在非叶子结点命中
    3. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
    4. 更适合文件索引系统
    5. B+树插入操作的平均时间复杂度为O(logn),最坏时间复杂度为O(logn)

    查找过程

    • 在 B+ 树上,既可以进行缩小范围的查找,也可以进行顺序查找;
    • 在进行缩小范围的查找时,不管成功与否,都必须查到叶子结点才能结束;
    • 若在结点内查找时,给定值≤Ki, 则应继续在 Ai 所指子树中进行查找

    插入和删除的操作:类似于B_树进行,即必要时,也需要进行结点的“分裂”或“合并”。

    为什么说B+tree比B树更适合实际应用中操作系统的文件索引和数据库索引?

    1. B+tree的磁盘读写代价更低
      • B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
      • 举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
    2. B+tree的查询效率更加稳定
      • 由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

    B树和B+树都是平衡的多叉树。B树和B+树都可用于文件的索引结构。B树和B+树都能有效的支持随机检索。B+树既能索引查找也能顺序查找.

    哈希表

    1. 在记录的存储地址和它的关键字之间建立一个确定的对应关系;这样不经过比较,一次存取就能得到元素。
    2. 哈希函数——在记录的关键字与记录的存储位置之间建立的一种对应关系。是从关键字空间到存储位置空间的一种映象。
    3. 哈希表——应用哈希函数,由记录的关键字确定记录在表中的位置信息,并将记录根据此信息放入表中,这样构成的表叫哈希表。
    4. Hash查找适合于关键字可能出现的值的集合远远大于实际关键字集合的情形。
    5. 更适合查找,不适合频繁更新
    6. Hash表等查找复杂依赖于Hash值算法的有效性,在最好的情况下,hash表查找复杂度为O(1)。只有无冲突的hash_table复杂度才是O(1)。一般是O(c),c为哈希关键字冲突时查找的平均长度。插入,删除,查找都是O(1)。平均查找长度不随表中结点数目的增加而增加,而是随负载因子的增大而增大
    7. 由于冲突的产生,使得哈希表的查找过程仍然是一个给定值与关键字比较的过程。

    根据抽屉原理,冲突是不可能完全避免的,所以,选择好的散列函数和冲突处理方法:

    1. 构造一个性能好,冲突少的Hash函数
    2. 如何解决冲突

    常用的哈希函数

    1. 直接定址法。仅适合于:地址集合的大小 == 关键字集合的大小
    2. 数字分析法。对关键字进行分析,取关键字的若干位或其组合作哈希地址。仅适合于:能预先估计出全体关键字的每一位上各种数字出现的频度。
    3. 平方取中法。以关键字的平方值的中间几位作为存储地址。
    4. 折叠法。将关键字分割成位数相同的几部分,然后取这几部分的叠加和(舍去进位)做哈希地址。移位叠加/间界叠加。适合于: 关键字的数字位数特别多,且每一位上数字分布大致均匀情况。
    5. 除留余数法。取关键字被某个不大于哈希表表长m的数p除后所得余数作哈希地址,即H(key)=key%p,p<=m。
    6. 随机数法。取关键字的伪随机函数值作哈希地址,即H(key)=random(key),适于关键字长度不等的情况。

    冲突解决

    1. 开放定址法。当冲突发生时,形成一个探查序列;沿此序列逐个地址探查,直到找到一个空位置(开放的地址),将发生冲突的记录放到该地址中。即Hi=(H(key)+di) % m,i=1,2,……k(k<=m-1),H(key)哈希函数,m哈希表长,di增量序列。缺点:删除:只能作标记,不能真正删除;溢出;载因子过大、解决冲突的算法选择不好会发生聚集问题。要求装填因子α较小,故当结点规模较大时会浪费很多空间。
      • 线性探测再散列:di=1,2,3,…,m-1
      • 二次探测再散列:di=12,-12,22,-22,…,±k2(k<=m/2)
      • 伪随机探测再散列: di为伪随机数序列
    2. 链地址法:将所有关键字为同义词的记录存储在一个单链表中,并用一维数组存放头指针。拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间。一旦发生冲突,在当前位置给单链表增加结点就行。
    3. 其他方法:再哈希法、建立公共溢出区
    4. 在用拉链法构造的散列表中,删除结点的操作易于实现。拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况。拉链法解决冲突时,需要使用指针,指示下一个元素的存储位置
    5. 开哈希表–链式地址法;闭哈希表–开放地址法.开哈希和闭哈希主要的区别在于,随着哈希表的密集度提高,使用闭哈希时,不仅会与相同哈希值的元素发生冲突,还容易与不同哈希值的元素发生冲突;而开哈希则不受哈希表疏密与否的影响,始终只会与相同哈希值的元素冲突而已。所以在密集度变大的哈希表中查找时,显然开哈希的平均搜索长度不会增长。
    6. 设有n个关键字具有相同的Hash函数值,则用线性探测法把这n个关键字映射到Hash表中需要做n*(n-1)/2次线性探测。如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行n*(n+1)/2次探测

    Hash查找效率:装填因子=表中记录数/表容量

    有B+Tree/Hash_Map/STL Map三种数据结构。对于内存中数据,查找性能较好的数据结构是Hash_Map,对于磁盘中数据,查找性能较好的数据结构是B+Tree。Hash操作能根据散列值直接定位数据的存储地址,设计良好的hash表能在常数级时间下找到需要的数据,但是更适合于内存中的查找。B+树是一种是一种树状的数据结构,适合做索引,对磁盘数据来说,索引查找是比较高效的。STL_Map的内部实现是一颗红黑树,但是只是一颗在内存中建立二叉树树,不能用于磁盘操作,而其内存查找性能也比不上Hash查找。

    内部排序

    1. 内部排序:全部数据可同时放入内存进行的排序。
    2. 外部排序:文件中数据太多,无法全部调入内存进行的排序。

    插入类:

    1. 直接插入排序。最坏情况是数据递减序,数据比较和移动量最大,达到O(n2),最好是数据是递增序,比较和移动最少为O(n)。趟数是固定的n-1,即使有序,也要依次从第二个元素开始。排序趟数不等于时间复杂度。
    2. 折半插入排序 。由于插入第i个元素到r[1]到r[i-1]之间时,前i个数据是有序的,所以可以用折半查找确定插入位置,然后插入。
    3. 希尔排序。缩小增量排序。5-3-1。在实际应用中,步长的选取可简化为开始为表长n的一半(n/2),以后每次减半,最后为1。插入的改进,最后一趟已基本有序,比较次数和移动次数相比直接插入最后一趟更少

    交换类:

    1. 冒泡排序。O(n2)通常认为冒泡是比较差的,可以加些改进,比如在一趟中无数据的交换,则结束等措施。
      • 在数据已基本有序时,冒泡是一个较好的方法
      • 在数据量较少时(15个左右)可以用冒泡
    2. 快速排序。
      • 时间复杂度。最好情况:每次支点总在中间,O(nlog2n),平均O(nlog2n)。最坏,数据已是递增或递减,O(n2)。pivotkey的选择越靠近中央,即左右两个子序列长度越接近,排序速度越快。越无序越快。
      • 空间复杂度。需栈空间以实现递归,最坏情况:S(n)=O(n);一般情况:S(n)=O(log2n)
      • 在序列已是有序的情况下,时间复杂度最高。原因:支点选择不当。改进:随机选取支点或最左、最右、中间三个元素中的值处于中间的作为支点,通常可以避免最坏情况。所以,快速排序在表已基本有序的情况下不合适。
      • 在序列长度已较短时,采用直接插入排序、起泡排序等排序方法。序列的个数通常取10左右。

    选择类排序:

    1. 简单选择排序。O(n2)。总比较次数n(n-1)/2。
    2. 堆排序。建堆 O(n),筛选排序O(nlogn)。找出若干个数中最大/最小的前K个数,用堆排序是最好。小根堆中最大的数一定是放在叶子节点上,堆本身是个完全二叉树,完全二叉树的叶子节点的位置大于[n/2]。时间复杂度不会因为待排序序列的有序程度而改变,但是待排序序列的有序程度会影响比较次数。
    3. 归并排序。时间:与表长成正比,若一个表表长是m,另一个是n,则时间是O(m+n)。单独一个数组归并,时间:O(nlogn),空间:O(n),比较次数介于(nlogn)/2和(nlogn)-n+1,赋值操作的次数是(2nlogn)。归并排序算法比较占用内存,但却是效率高且稳定的排序算法。在外排序中使用。归并的趟数是logn。
    4. 基数排序。在一般情况下,每个结点有 d 位关键字,必须执行 t = d次分配和收集操作。分配的代价:O(n);收集的代价:O(rd) (rd是基数);总的代价为:O( d ×(n + rd))。适用于以数字和字符串为关键字的情况。
    5. 枚举排序,通常也被叫做秩排序,比较计数排序。对每一个要排序的元素,统计小于它的所有元素的个数,从而得到该元素在整个序列中的位置,时间复杂度为O(n2)

    比较法分类的下界:O(nlogn)

    排序算法的一些特点:

    1. 堆排序、冒泡排序、快速排序在每趟排序过程中,都会有一个元素被放置在其最终的位置上。
    2. 有字符序列 {Q,H,C,Y,P,A,M,S,R,D,F,X} ,新序列{F,H,C,D,P,A,M,Q,R,S,Y,X},是快速排序算法一趟扫描的结果。(拿Q作为分割点,快速排序一轮。二路归并,第一趟排序,得到 n / 2 个长度为 2 的各自有序的子序列,第二趟排序,得到 n / 4 个长度为 4 的各自有序的子序列H Q C Y A P M S D R F X。如果是快速排序的话,第一个元素t将会被放到一个最准确的位置,t前的数均小于t,后面的数均大于t。希尔排序每个小分组内将会是有序的。堆排序,把它构成一颗二叉树的时候,该堆要么就是大根堆,要么就是小根堆,第一趟Y排在最后;冒泡,那么肯定会有数据下沉的动作,第一趟有A在第一位。)
    3. 在文件”局部有序”或文件长度较小的情况下,最佳内部排序的方法是直接插入排序。(归并排序要求待排序列已经部分有序,而部分有序的含义是待排序列由若干有序的子序列组成,即每个子序列必须有序,并且其时间复杂度为O(nlog2n);直接插入排序在待排序列基本有序时,每趟的比较次数大为降低,即n-1趟比较的时间复杂度由O(n^2)降至O(n)。在待排序的元素序列基本有序或者每个元素距其最终位置不远也可用插入排序,效率最高的排序方法是插入排序
    4. 排序趟数与序列的原始状态有关的排序方法是优化冒泡和快速排序法。(插入排序和选择排序不管序列的原始状态是什么都要执行n-1趟,优化冒泡和快排不一定。仔细理解排序的次数比较次数的区别)
    5. 不稳定的排序方法:快排,堆排,希尔,选择
    6. 要与关键字的初始排列次序无关,那么就是最好、最坏、一般的情况下排序时间复杂度不变, 总共有堆排序,归并排序,选择排序,基数排序
    7. 快速排序、Shell 排序、归并排序、直接插入排序的关键码比较次数与记录的初始排列有关。折半插入排序、选择排序无关。(直接插入排序在完全有序的情况下每个元素只需要与他左边的元素比较一次就可以确定他最终的位置;折半插入排序,比较次数是固定的,与初始排序无关;快速排序,初始排序不影响每次划分时的比较次数,都要比较n次,但是初始排序会影响划分次数,所以会影响总的比较次数,但快排平均比较次数最小;归并排序在归并的时候,如果右路最小值比左路最大值还大,那么只需要比较n次,如果右路每个元素分别比左路对应位置的元素大,那么需要比较2*n-1次,所以与初始排序有关)
    8. 精俭排序,即一对数字不进行两次和两次以上的比较,插入和归并是“精俭排序”。插入排序,前面是有序的,后面的每一个元素与前面有序的元素比较,比较过的就是有序的了,不会再比较一次。归并每次合并后,内部都是有序的,内部的元素之间不用再比较。选择排序,每次在后面的元素中找到最小的,找最小元素的过程是在没有排好序的那部分进行,所有肯定会比较多次。堆排序也需比较多次。

    外部排序

    1. 生成合并段(run):读入文件的部分记录到内存->在内存中进行内部排序->将排好序的这些记录写入外存,形成合并段->再读入该文件的下面的记录,往复进行,直至文件中的记录全部形成合并段为止。
    2. 外部合并:将上一阶段生成的合并段调入内存,进行合并,直至最后形成一个有序的文件。
    3. 外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行多路归并排序
    4. 不管初始序列是否有序, 冒泡、选择排序时间复杂度是O(n^2),归并、堆排序时间复杂度是O(nlogn)
    5. 外部排序的总时间 = 内部排序(产出初始归并段)所需时间 + 外存信息读取时间 + 内部归并所需的时间
    6. 外排中使用置换选择排序的目的,是为了增加初始归并段的长度。减少外存读写次数需要减小归并趟数

    7. 根据内存容量设若干个输入缓冲区和一个输出缓冲区。若采用二路归并,用两个输入缓冲。

    8. 归并的方法类似于归并排序的归并算法。增加的是对缓冲的监视,对于输入,一旦缓冲空,要到相应文件读后续数据,对于输出缓冲,一旦缓冲满,要将缓冲内容写到文件中去。
    9. 外排序和内排序不只是考虑内外排序算法的性能,还要考虑IO数据交换效率的问题,内存存取速度远远高于外存。影响外排序的时间因素主要是内存与外设交换信息的总次数

    有效的算法设计

    1. 贪心法。Dijkstra的最短路径(时间复杂度O(n2));Prim求最小生成树邻接表存储时是O(n+e),图O(n2);关键路径及关键活动的求法。
    2. 回溯法
    3. 分支限界法
    4. 分治法。分割、求解、合并。二分查找、归并排序、快速排序。
    5. 动态规划。Floyd-Warshall算法求解图中所有点对之间最短路径时间复杂度为O(n3)

    动态规划解题的方法是一种高效率的方法,其时间复杂度通常为O(n2),O(n3)等,可以解决相当大的信息量。(数塔在n<=100层时,可以在很短的时间内得到问题解)

    • 适用的原则:原则为优化原则,即整体优化可以分解为若干个局部优化。
    • 动态规划比穷举法具有较少的计算次数
    • 递归算法需要很大的栈空间,而动态规划不需要栈空间

    贪心和动态规划的差别:

    1. 所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。
    2. 在动态规划算法中,每步所作的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能作出选择。而在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择。然后再去解作出这个选择后产生的相应的子问题。
    3. 贪心算法所作的贪心选择可以依赖于以往所作过的选择,但决不依赖于将来所作的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各子问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为一个规模更小的子问题。

    P问题

    1. P问题,如果它可以通过运行多项式次(即运行时间至多是输入量大小的多项式函数的一种算法获得解决),可以找到一个能在多项式的时间里解决它的算法。—-确定性问题
    2. NP问题,虽然可以用计算机求解,但是对于任意常数k,它们不能在O(nk)时间内得到解答,可以在多项式的时间里验证一个解的问题。所有的P类问题都是NP问题。
    3. NP完全问题,知道有效的非确定性算法,但是不知道是否存在有效的确定性算法,同时,不能证明这些问题中的任何一个不存在有效的确定性算法。这类问题称为NP完全问题。
    展开全文
  • 数据结构知识整理

    万次阅读 多人点赞 2018-07-30 18:50:47
    1.数据结构:是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。 2.数据结构涵盖的内容: 3.基本概念和术语: 数据:对客观事物的符号表示,在计算机科学中是指所有能...

    基于严蔚敏及吴伟民编著的清华大学C语言版教材并结合网上相关资料整理(http://www.docin.com/p-2027739005.html)

    第一章:绪论

    1.数据结构:是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。

    2.数据结构涵盖的内容:

    3.基本概念和术语:

    数据:对客观事物的符号表示,在计算机科学中是指所有能输入到计算机中并被计算机程序处理的符号的总称。

    数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。

    数据对象:性质相同的数据元素的集合,是数据的一个子集。

    数据结构:相互之间存在一种或多种特定关系的数据元素的集合。

    数据类型:一个值的集合和定义在这个值集上的一组操作的总称。

    4.算法和算法分析

    1)算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。

    算法五个特性:有穷性,确定性,可行性,输入,输出。

    2)算法设计要求:正确性,可读性,健壮性,效率与低存储量需求。

    3)算法分析:时间复杂度,空间复杂度,稳定性

     

    第二章:线性表

    1.线性结构特点:在数据元素的非空有限集合中,(1)存在唯一的一个被称做“第一个”的数据元素;(2)存在唯一的一个被称做“最后一个”的数据元素;(3)除第一个之外,集合中的每个数据元素均只有一个前驱;(4)除最后一个之外,集合中每个数据元素均只有一个后继。

    2.线性表定义:有限个性质相同的数据元素组成的序列。

    3.线性表的存储结构:顺序存储结构和链式存储结构

    顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构。

    通常用数组来描述数据结构中的顺序存储结构。

    链式存储结构: 其结点在存储器中的位置是随意的,即逻辑上相邻的数据元素在物理上不一定相邻。通过指针来实现。

    数据结构的基本运算:修改、插入、删除、查找、排序

    4.线性表的顺序表示和实现

    1)修改:通过数组的下标便可访问某个特定元素并修改。

    时间复杂度O(1)

    2) 插入:在线性表的第i个位置前插入一个元素

    实现步骤:

       ①将第n至第i 位的元素逐一向后移动一个位置;

       ②将要插入的元素写到第i个位置;

       ③表长加1。

       注意:事先应判断: 插入位置i 是否合法?表是否已满?

       应当符合条件: 1≤i≤n+1  或  i=[1, n+1]

       核心语句:

    for (j=n; j>=i; j--)

    a[j+1]=a[ j ]; 

    a[ i ]=x;   

    n++;

      插入时的平均移动次数为:n(n+1)/2÷(n+1)=n/2≈O(n)

    3)删除:删除线性表的第i个位置上的元素

      实现步骤:

      ①将第i+1 至第n 位的元素向前移动一个位置;

      ②表长减1。

      注意:事先需要判断,删除位置i 是否合法?

      应当符合条件:1≤i≤n  或  i=[1, n]

      核心语句:

    {

        for ( j=i+1; j<=n; j++ )

        a[j-1]=a[j]; 

        n--;

    }

      顺序表删除一元素的时间效率为:T(n)=(n-1)/2 O(n)

      顺序表插入、删除算法的平均空间复杂度为O(1)

    5.线性表的链式表示和实现

    线性链表:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

    一个数据元素称为一个结点,包括两个域:存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。

    由于链表的每个结点中只包含一个指针域,故线性链表又称为单链表。

    1)单链表的修改(或读取)

    思路:要修改第i个数据元素,必须从头指针起一直找到该结点的指针preturn p;  

    然后才能:p->data=new_value

    读取第i个数据元素的核心语句是:

    Linklist *find(Linklist *head ,int i)

    { 

       int j=1;

       Linklist *p;

       P=head->next;

       While((p!=NULL)&&(j<i))

       {

           p=p->next;

           j++;

        }

    }

    2)单链表的插入

    链表插入的核心语句:

    Step 1:s->next=p->next;

    Step 2:p->next=s;

    3)单链表的删除

    删除动作的核心语句(要借助辅助指针变量q):

    q = p->next;           //首先保存b的指针,靠它才能找到c;

    p->next=q->next;  //将a、c两结点相连,淘汰b结点;

    free(q) ;               //彻底释放b结点空间

    4)双向链表的插入操作

    设p已指向第i 元素,请在第 i 元素前插入元素 x:

    ① ai-1的后继从 ai ( 指针是p)变为 x(指针是s) :

                    s->next = p  ;   p->prior->next = s ;

    ② ai  的前驱从ai-1 ( 指针是p->prior)变为 x ( 指针是s);

                    s->prior = p ->prior ; p->prior = s ;

    5)双向链表的删除操作

    设p指向第i 个元素,删除第 i 个 元素

    后继方向:ai-1的后继由ai ( 指针p)变为ai+1(指针 p ->next );

                       p ->prior->next =  p->next  ;

    前驱方向:ai+1的前驱由ai ( 指针p)变为ai-1 (指针 p -> prior );

                     p->next->prior = p ->prior ;

    6.循环链表

    循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

    循环链表的操作和线性链表基本一致,差别仅在于算法中的循环条件不是p或p->next是否为空,而是它们是否等于头指针。

    学习重点:

    • 线性表的逻辑结构,指线性表的数据元素间存在着线性关系。在顺序存储结构中,元素存储的先后位置反映出这种线性关系,而在链式存储结构中,是靠指针来反映这种关系的。

    • 顺序存储结构用一维数组表示,给定下标,可以存取相应元素,属于随机存取的存储结构。

    • 链表操作中应注意不要使链意外“断开”。因此,若在某结点前插入一个元素,或删除某元素,必须知道该元素的前驱结点的指针。

    • 掌握通过画出结点图来进行链表(单链表、循环链表等)的生成、插入、删除、遍历等操作。

    • 数组(主要是二维)在以行序/列序为主的存储中的地址计算方法。

    补充重点:

    • 每个存储结点都包含两部分:数据域和指针域(链域)

    • 在单链表中,除了首元结点外,任一结点的存储位置由 其直接前驱结点的链域的值 指示。

    • 在链表中设置头结点有什么好处?

        头结点即在链表的首元结点之前附设的一个结点,该结点的数据域可以为空,也可存放表长度等附加信息,其作用是为了对链表进行操作时,可以对空表、非空表的情况以及对首元结点进行统一处理,编程更方便。

    • 如何表示空表?

    (1)无头结点时,当头指针的值为空时表示空表;

    (2)有头结点时,当头结点的指针域为空时表示空表。

    • 链表的数据元素有两个域,不再是简单数据类型,编程时该如何表示?

     因每个结点至少有两个分量,且数据类型通常不一致,所以要采用结构数据类型。

    • sizeof(x)——  计算变量x的长度(字节数);

              malloc(m) —  开辟m字节长度的地址空间,并返回这段空间的首地址;

              free(p)   —— 释放指针p所指变量的存储空间,即彻底删除一个变量。

    • 链表的运算效率分析:

    (1)查找 

    因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。

    (2) 插入和删除 

    因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。

    但是,如果要在单链表中进行前插或删除操作,因为要从头查找前驱结点,所耗时间复杂度将是 O(n)。

    例:在n个结点的单链表中要删除已知结点*P,需找到它的前驱结点的地址,其时间复杂度为 O(n)

    • 顺序存储和链式存储的区别和优缺点?

        顺序存储时,逻辑上相邻的数据元素,其物理存放地址也相邻。顺序存储的优点是存储密度大,存储空间利用率高;缺点是插入或删除元素时不方便。

      链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针。链式存储的优点是插入或删除元素时很方便,使用灵活。缺点是存储密度小,存储空间利用率低。

    • 顺序表适宜于做查找这样的静态操作;

    • 链表宜于做插入、删除这样的动态操作。

    • 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;

    • 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表。

    ① 数组中各元素具有统一的类型;

    ② 数组元素的下标一般具有固定的上界和下界,即数组一旦被定义,它的维数和维界就不再改变。

    ③数组的基本操作比较简单,除了结构的初始化和销毁之外,只有存取元素和修改元素值的操作。

    • 三元素组表中的每个结点对应于稀疏矩阵的一个非零元素,它包含有三个数据项,分别表示该元素的 行下标  、列下标 和 元素值 。

    第三章:栈和队列

    1.栈:限定仅在表尾进行插入或删除操作的线性表。

    栈的基本操作:在栈顶进行插入或删除,栈的初始化、判空及取栈顶元素等。

    入栈口诀:堆栈指针top “先压后加”

    出栈口诀:堆栈指针top “先减后弹”

    top=0表示空栈。

     

    2.栈的表示和实现

         1)构造一个空栈S

        Status InitStack(SqStack &S)

        {

            S.base = (SElemType *) malloc(STACK_INIT_SIZE * sizeof(SElemType));

            if(!S.base) exit (OVERFLOW); //存储分配失败

                S.top = S.base;

                S.stacksize = STACK_INIT_SIZE;

                return OK;

        }

     

        2)返回栈顶元素

           Status GetTop(SqStack S, SElemType e) 

            {//若栈不空,则用e返回S的栈顶元素,并返回OK,否则返回ERROR

                if(S.top == S.base) return ERROR;

                e = *(S.top-1);

                return OK; 

            }//GetTop

     

        3)顺序栈入栈函数PUSH()

    Status Push(SqStack &S, SElemType e)

    { //插入元素e为新的栈顶元素

        if(S.top-S.base>=S.stacksize)//栈满,追加存储空间

       {

        s.base = (SElemType*)realloc(S.base,(S.stacksize+STACKINCREMENT)*sizeof(SElemType));

        if(!S.base) exit(OVERFLOW);//存储分配失败

        S.top = S.base + S.stacksize;

        S.stacksize += STACKINCREMENT;

        }

        *S.top++ =e;

        return OK:

    }//PUSH

    4)顺序栈出栈函数POP()

    status Pop( SqStack &S,SElemType &e)

    { //若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR

        if(S.top == S.base) return ERROR; 

        e=* —S.top;

        return OK;

            }

    3.栈的应用

    数制转换,括号匹配的检验,行编辑程序,迷宫求解,表达式求值,递归实现。

    4.队列:是一种先进先出的线性表,它只允许在表的一端进行插入,而在另一端删除元素。

    允许插入的一端叫做队尾,允许删除的一端叫做队头。

    除了栈和队列外,还有一种限定性数据结构是双端队列。双端队列是限定插入和删除操作在表的两端进行的线性表。

    5.链队列结点类型定义:

     typedef Struct QNode{

         QElemType        data;     //元素

         Struct   QNode   *next;  //指向下一结点的指针

       }Qnode , * QueuePtr ;

    链队列类型定义:

     typedef  struct {

      QueuePtr     front ; //队首指针

      QueuePtr     rear ; //队尾指针

      }  LinkQueue;

    链队示意图:

    ①  空链队的特征:front=rear

    ②  链队会满吗?一般不会,因为删除时有free动作。除非内存不足!

    ③  入队(尾部插入):rear->next=S; rear=S;

        出队(头部删除):front->next=p->next;

    6.顺序队

    顺序队类型定义:

    #define    QUEUE-MAXSIZE  100  //最大队列长度

         typedef    struct {

                QElemType     *base;    //队列的基址

                 int                    front;     //队首指针

                 int                     rear;    //队尾指针

           }SqQueue

    建队核心语句:

    q.base=(QElemType *)malloc(sizeof (QElemType)* QUEUE_MAXSIZE);       //分配空间

    顺序队示意图:

    7.循环队列:

    队空条件 :  front = rear       (初始化时:front = rear )

    队满条件: front = (rear+1) % N         (N=maxsize)

    队列长度(即数据元素个数):L=(N+rear-front)% N

    1)初始化一个空队列

    Status   InitQueue ( SqQueue  &q ) //初始化空循环队列 q

    {   

    q.base=(QElemType *)malloc(sizeof(QElemType)* QUEUE_MAXSIZE);   //分配空间

    if (!q.base)  exit(OVERFLOW);//内存分配失败,退出程序

       q.front =q.rear=0; //置空队列

        return OK;

     } //InitQueue;

     

    2)入队操作

    Status   EnQueue(SqQueue  &q,  QElemType e)

    {//向循环队列 q 的队尾加入一个元素 e

       if ( (q.rear+1) %  QUEUE_MAXSIZE ==  q.front)

                        return  ERROR ; //队满则上溢,无法再入队

            q.rear = ( q.rear + 1 ) %  QUEUE_MAXSIZE;

            q.base [q.rear] = e;    //新元素e入队

            return  OK;

     }// EnQueue;

     

    3)出队操作

    Status     DeQueue ( SqQueue  &q,    QElemType &e)

     {//若队列不空,删除循环队列q的队头元素,

            //由 e 返回其值,并返回OK

          if ( q.front = = q.rear )   return ERROR;//队列空

          q.front=(q.front+1) % QUEUE_MAXSIZE; 

          e = q.base [ q.front ] ;

         return OK;

        }// DeQueue

    • 链队列空的条件是首尾指针相等,而循环队列满的条件的判定,则有队尾加1等于队头和设标记两种方法。

    补充重点:

    1. 为什么要设计堆栈?它有什么独特用途?

    2. 什么叫“假溢出” ?如何解决?

     调用函数或子程序非它莫属;

     递归运算的有力工具;

     用于保护现场和恢复现场;

     简化了程序设计的问题。                         

    2.为什么要设计队列?它有什么独特用途?

     离散事件的模拟(模拟事件发生的先后顺序,例如 CPU芯片中的指令译码队列);

     操作系统中的作业调度(一个CPU执行多个作业);

     简化程序设计。

    答:在顺序队中,当尾指针已经到了数组的上界,不能再有入队操作,但其实数组中还有空位置,这就叫“假溢出”。解决假溢出的途径———采用循环队列。

    4.在一个循环队列中,若约定队首指针指向队首元素的前一个位置。那么,从循环队列中删除一个元素时,其操作是先 移动队首位置 ,后 取出元素

    5.线性表、栈、队的异同点:

    相同点:逻辑结构相同,都是线性的;都可以用顺序存储或链表存储;栈和队列是两种特殊的线性表,即受限的线性表(只是对插入、删除运算加以限制)。

    不同点:① 运算规则不同:

    线性表为随机存取;

    而栈是只允许在一端进行插入和删除运算,因而是后进先出表LIFO

    队列是只允许在一端进行插入、另一端进行删除运算,因而是先进先出表FIFO

     用途不同,线性表比较通用;堆栈用于函数调用、递归和简化设计等;队列用于离散事件模拟、OS作业调度和简化设计等。

     

    第四章:串

    1.串是数据元素为字符的线性表,串的定义及操作。

    串即字符串,是由零个或多个字符组成的有限序列,是数据元素为单个字符的特殊线性表。

    串比较:int strcmp(char *s1,char *s2);            

    求串长:int strlen(char *s);                          

    串连接:char  strcat(char *to,char *from)   

    子串T定位:char strchr(char *s,char *c);

    2.串的存储结构,因串是数据元素为字符的线性表,所以存在“结点大小”的问题。

    模式匹配算法 

    串有三种机内表示方法:

     

    3.模式匹配算法 :

    算法目的:确定主串中所含子串第一次出现的位置(定位)

    定位问题称为串的模式匹配,典型函数为Index(S,T,pos)

    BF算法的实现—即编写Index(S, T, pos)函数

    BF算法设计思想:

    将主串S的第pos个字符和模式T的第1个字符比较,

    若相等,继续逐个比较后续字符;

    若不等,从主串S的下一字符(pos+1)起,重新与T第一个字符比较。 

    直到主串S的一个连续子串字符序列与模式T相等。返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功。

    否则,匹配失败,返回值 0。

    Int Index_BP(SString S, SString T, int pos)

    { //返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0.

     // 其中,T非空,1≤pos≤StrLength(S)

        i=pos;      j=1;

       while ( i<=S[0] && j<=T[0] ) //如果i,j二指针在正常长度范围,

         {  

           if (S[i] = = T[j] ) {++i, ++j; }   //则继续比较后续字符

           else {i=i-j+2; j=1;} //若不相等,指针后退重新开始匹配

          }

      if(j>T[0]) return i-T[0];  //T子串指针j正常到尾,说明匹配成功,  else return 0;        //否则属于i>S[0]情况,i先到尾就不正常

    } //Index_BP

    补充重点:

    1.空串和空白串有无区别?

    答:有区别。

    空串(Null String)是指长度为零的串;

    而空白串(Blank  String),是指包含一个或多个空白字符‘  ’(空格键)的字符串.

    2.“空串是任意串的子串;任意串S都是S本身的子串,除S本身外,S的其他子串称为S的真子串。”

     

    第五章:数组和广义表

    重点:二维数组的位置计算。

    矩阵的压缩存储:特殊矩阵(三角矩阵,对称矩阵),稀疏矩阵,

     

    第六章:树和二叉树

    1.树:是n(n≥0)个结点的有限集。(1)有且仅有一个特定的称为根(root)的结点;(2)当n>1时,其余的结点分为m(m≥0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。

    2.二叉树:是n(n≥0)个结点的有限集合,由一个根结点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。

    二叉树的性质,存储结构。

    性质1: 在二叉树的第i层上至多有2^(i-1)个结点(i>0)。

    性质2: 深度为k的二叉树至多有2^k-1个结点(k>0)。

    性质3: 对于任何一棵二叉树,如果其终端结点数为n0,度为2的结点数有n2个,则叶子数n0=n2+1

    性质4: 具有n个结点的完全二叉树的深度必为 [log2n]+1

    性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)。

    二叉树的存储结构:

    1).顺序存储结构

    按二叉树的结点“自上而下、从左至右”编号,用一组连续的存储单元存储。

    若是完全/满二叉树则可以做到唯一复原。

    不是完全二叉树:一律转为完全二叉树!

    方法很简单,将各层空缺处统统补上“虚结点”,其内容为空。

    缺点:①浪费空间;②插入、删除不便 

    2).链式存储结构

    用二叉链表即可方便表示。一般从根结点开始存储。

    lchild

    data

    rchild

    优点:①不浪费空间;②插入、删除方便

    3.二叉树的遍历。

    指按照某种次序访问二叉树的所有结点,并且每个结点仅访问一次,得到一个线性序列。

    遍历规则:二叉树由根、左子树、右子树构成,定义为D、 L、R

    若限定先左后右,则有三种实现方案:

      DLR               LDR                   LRD

    先序遍历           中序遍历          后序遍历

    4.线索二叉树

    1)线索二叉树可以加快查找前驱与后继结点,实质就是将二叉链表中的空指针改为指向前驱或后继的线索,线索化就是在遍历中修改空指针。

    通常规定:对某一结点,若无左子树,将lchild指向前驱结点;若无右子树,将rchild指向后继结点。

    还需要设置左右两个tag,用来标记当前结点是否有子树。

    若ltag==1,lchild指向结点前驱;若rtag==1,rchild指向结点后继。

    2)线索二叉树的存储结构如下:

    typedef struct ThreadNode{

    ElemType data;

    struct ThreadNode *lchild, *rchild;

    int ltag, rtag;

    }ThreadNode, *ThreadTree;

    5.树和森林

    1)树有三种常用存储方式:

    ①双亲表示法     ②孩子表示法    ③孩子—兄弟表示法

    2)森林、树、二叉树的转换

    (1)将树转换为二叉树

    树中每个结点最多只有一个最左边的孩子(长子)和一个右邻的兄弟。按照这种关系很自然地就能将树转换成相应的二叉树:a.在所有兄弟结点之间加一连线

    b.对每个结点,除了保留与其长子的连线外,去掉该结点与其它孩子的连线。

    (2)将一个森林转换为二叉树:

    具体方法是:a.将森林中的每棵树变为二叉树;

    b.因为转换所得的二叉树的根结点的右子树均为空,故可将各二叉树的根结点视为兄弟从左至右连在一起,就形成了一棵二叉树。

    (3)二叉树转换为树

    是树转换为二叉树的逆过程。

    a.加线。若某结点X的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点,都作为结点X的孩子。将结点X与这些右孩子结点用线连接起来。

    b.去线。删除原二叉树中所有结点与其右孩子结点的连线。

     

    (4)二叉树转换为森林:

    假如一棵二叉树的根节点有右孩子,则这棵二叉树能够转换为森林,否则将转换为一棵树。

    a.从根节点开始,若右孩子存在,则把与右孩子结点的连线删除。再查看分离后的二叉树,若其根节点的右孩子存在,则连线删除。直到所有这些根节点与右孩子的连线都删除为止。

    b.将每棵分离后的二叉树转换为树。

    6.树和森林的遍历

    • 树的遍历

    ① 先根遍历:访问根结点;依次先根遍历根结点的每棵子树。

    ② 后根遍历:依次后根遍历根结点的每棵子树;访问根结点。

     

    • 森林的遍历

    ① 先序遍历

    若森林为空,返回;

    访问森林中第一棵树的根结点;

    先根遍历第一棵树的根结点的子树森林;

    先根遍历除去第一棵树之后剩余的树构成的森林。

    ② 中序遍历

    若森林为空,返回;

    中根遍历森林中第一棵树的根结点的子树森林;

    访问第一棵树的根结点;

    中根遍历除去第一棵树之后剩余的树构成的森林。

     

    7.哈夫曼树及其应用

    Huffman树:最优二叉树(带权路径长度最短的树)

    Huffman编码:不等长编码。

    树的带权路径长度:(树中所有叶子结点的带权路径长度之和)

    构造Huffman树的基本思想:权值大的结点用短路径,权值小的结点用长路径。

    构造Huffman树的步骤(即Huffman算法):

    (1) 由给定的 n 个权值{ w1, w2, …, wn }构成n棵二叉树的集合F = { T1, T2, …, Tn } (即森林) ,其中每棵二叉树 Ti 中只有一个带权为 wi 的根结点,其左右子树均空。

    (2) 在F 中选取两棵根结点权值最小的树 做为左右子树构造一棵新的二叉树,且让新二叉树根结点的权值等于其左右子树的根结点权值之和。

    (3) 在F 中删去这两棵树,同时将新得到的二叉树加入 F中。

    (4) 重复(2) 和(3) , 直到 F 只含一棵树为止。这棵树便是Huffman树。

    具体操作步骤:

    应用:用于通信编码

      在通信及数据传输中多采用二进制编码。为了使电文尽可能的缩短,可以对电文中每个字符出现的次数进行统计。设法让出现次数多的字符的二进制码短些,而让那些很少出现的字符的二进制码长一些。假设有一段电文,其中用到 4 个不同字符A,C,S,T,它们在电文中出现的次数分别为 7 , 2 , 4 , 5 。把 7 , 2 , 4 , 5 当做 4 个叶子的权值构造哈夫曼树如图(a) 所示。在树中令所有左分支取编码为 0 ,令所有右分支取编码为1。将从根结点起到某个叶子结点路径上的各左、右分支的编码顺序排列,就得这个叶子结点所代表的字符的二进制编码,如图(b) 所示。这些编码拼成的电文不会混淆,因为每个字符的编码均不是其他编码的前缀,这种编码称做前缀编码。

     

    第七章 图

    1.图的定义,概念、术语及基本操作(https://blog.csdn.net/eyishion/article/details/53234255)

    1)图的定义

    图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成;

    通常表示为:G(V,E),G表示一个图,V是图G中顶点的集合,E是图G中边的集合;

    注意:在图中数据元素称之为顶点(Vertex),而且顶点集合有穷非空;在图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示。

    2)图的分类

    • 按照有无方向,分为无向图有向图

    无向图:如果图中任意两个顶点之间的边都是无向边,则称该图为无向图。

    无向边:若顶点M到顶点N的边没有方向,称这条边为无向边,用无序偶对(M,N)或(N,M)表示。

    无向图是有顶点构成。如下图所示就是一个无向图G1:

    无向图G1= (V1,{E1}),其中顶点集合 V1={A,B,C,D};边集合E1={(A,B),(B,C),(C,D),(D,A)}

    有向图:如果图中任意两个顶点之间的边都是有向边,则称该图为有向图。

    有向边:若顶点M到顶点N的边有方向,称这条边为有向边,也称为弧,用偶序对 < M, N >表示;M表示弧尾,N表示弧头

    有向图是有顶点构成,如下图所示是一个有向图G2:

    有向图G2=(V2,{E2}),其中顶点集合 V2={A,B,C,D};弧集合E2={< A,D>,< B,A>,< C,A>,< B,C>}

    对于弧< A,D>来说, A是弧尾,D是弧头

    注意:无向边用 小括号 “()”表示,有向边用“<>”表示。

    无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。

    含有n个顶点的无向完全图有n * (n-1)/2条边。下面是一个无向完全图

    4个顶点,6条无向边,每个顶点对应3条边 ,一共4个顶点 总共 4*3,每个顶点对应的边都重复计算了一次,所以整体要除以2。

    对于n各 顶点和e条边的无向图满足:0<=e <= n(n-1)/2

    有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。

    含有n个顶点的无向完全图有n * (n-1)条边。下面是一个有向完全图

    4个顶点,12条弧,一共4个顶点 总共 4*3。

    2,按照弧或边的多少,分为稀疏图稠密图

    若边或弧的个数e<=NlogN(N是顶点的个数),称为系数图,否则称为稠密图;

    3,按照边或弧是否带权,其中带权的图统称为

    有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权。

    有向网中弧带权的图叫做有向网;

    无向网中边带权的图叫做无向网;

    比如下图就是一个无向图

     

    图的顶点和边间关系

    邻接点 度 入度 出度

    对于无向图,假若顶点v和顶点w之间存在一条边,则称顶点v和顶点w互为邻接点,边(v,w)和顶点v和w相关联。

    顶点v的是和v相关联的边的数目,记为TD(v);

    上面这个无向图G1,A和B互为邻接点,A和D互为邻接点,B和C互为邻接点,C和D互为邻接点;

    A的度是2,B的度是2,C的度是2,D的度是2;所有顶点度的和为8,而边的数目是4;

    图中边的数目e = 各个顶点度数和的一半。

    对于有向图来说,与某个顶点相关联的弧的数目称为度(TD);以某个顶点v为弧尾的弧的数目定义为顶点v的出度(OD);以顶点v为弧头的弧的数目定义为顶点的入度(ID)

    度(TD) = 出度(OD) + 入度(ID);

    比如上面有向图,

    A的度为3 ,A的入度 2,A的出度是1

    B的度为2 ,B的入度 0,B的出度是2

    C的度为2 ,C的入度 1,C的出度是1

    D的度为1 ,D的入度 1,D的出度是0

    所有顶点的入度和是4,出度和也是4,而这个图有4个弧

    所以 有向图的弧 e = 所有顶点入度的和 = 所有顶点出度的和

    路径 路径长度 简单路径 回路 (环) 简单回路(简单环)

    设图G=(V,{E})中的一个顶点序列{u=Fi0,Fi1,Fi2,….Fim=w}中,(Fi,j-1,Fi,j)∈E 1 ≤j ≤m,则称从顶点u到顶点w之间存在一条路径,路径上边或弧的数目称作路径长度

    若路径中的顶点不重复出现的路径称为简单路径

    若路径中第一个顶点到最后一个顶点相同的路径称为回路或环

    若路径中第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环

    比如下图 :

    从B 到 D 中顶点没有重复出现 ,所以是简单路径 ,边的数目是2,所以路径长度为 2。

    图1和图2都是一个回路(环),图1中出了第一个顶点和最后一个顶点相同之外,其余顶点不相同,所以是简单环(简单回路),图2,有与顶点C重复就不是一个简单环了;

    连通图相关术语

    连通图

    在无向图G(V,{E})中,如果从顶点V到顶点W有路径,则称V和W是连通的。如果对于图中任意两个顶点Vi、Vj∈V,Vi和Vj都是连通的,则称G是连通图。

    如下图所示:

    图1,顶点A到顶点E就无法连通,所以图1不是连通;图2,图3,图4属于连通图;

    连通分量

    若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量;

    图1是无向非连通图,由两个连通分量,分别是图2和图3。图4尽管也是图1的子图,但是它不满足极大连通,也就说极大连通应当是包含ABCD四个顶点,比如图2那样;

    强连通图

    在有向图G(V,{E})中,如果对于每一对Vi ,Vj∈V,Vi≠Vj,从Vi到Vj和从Vj到Vi都存在有向路径,则称G是强连通图。

    图1不是强连通图因为D到A不存在路径,图2属于强连通图。

    强连通分量

    若有向图不是强连通图,则图中各个极大强连通子图称作此图的强连通分量;

    图1不是强连通图,但是图2是图1的强连通子图,也就是强连通分量;

    生成树和生成森林

    生成树

    假设一个连通图有n个顶点和e条边,其中n-1条边和n个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树;

    图1是一个连通图含有4个顶点和4条边,图2,图3,图4含有3条边和4个顶点,构成了一个极小连通图子图,也称为生成树,为什么是极小连通子图,因为图2,图3,图4中少一条边都构不成一个连通图,多一条边就变成一个回路(环),所以是3条边和4个顶点构成的极小连通子图。图5尽管也是3个边4个顶点,但不是连通图。

    生成森林

    如果一个有向图恰有一个顶点的入度为0,其余顶点的入度为1,则是一颗有向树

    入度为0,相当于根节点,入度为1,相当于分支节点;,比如下面的有向图就是一个有向树

    顶点B的入度是0,其余顶点的入度是1;

    一个有向图的生成森林由若干颗有向树组成,含有图中全部顶点,但有足以构成若干颗不相交的有向树的弧;

    有向图1去掉一些弧后分解成2颗有向树,图2和图3,这两颗树就是有向图图1的生成森林;

    2.图的存储结构

    1).邻接矩阵(数组)表示法

    ① 建立一个顶点表和一个邻接矩阵

    ② 设图 A = (V, E) 有 n 个顶点,则图的邻接矩阵是一个二维数组 A.Edge[n][n]。

    注:在有向图的邻接矩阵中,

       第i行含义:以结点vi为尾的弧(即出度边);

       第i列含义:以结点vi为头的弧(即入度边)。

    邻接矩阵法优点:容易实现图的操作,如:求某顶点的度、判断顶点之间是否有边(弧)、找顶点的邻接点等等。

    邻接矩阵法缺点:n个顶点需要n*n个单元存储边(弧);空间效率为O(n^2)。

    2).邻接表(链式)表示法

    ① 对每个顶点vi 建立一个单链表,把与vi有关联的边的信息(即度或出度边)链接起来,表中每个结点都设为3个域:

    ② 每个单链表还应当附设一个头结点(设为2个域),存vi信息;

    ③ 每个单链表的头结点另外用顺序存储结构存储。

    邻接表的优点:空间效率高;容易寻找顶点的邻接点;

    邻接表的缺点:判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。

    3.图的遍历

    遍历定义:从已给的连通图中某一顶点出发,沿着一些边,访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。

    图的遍历算法求解图的连通性问题、拓扑排序和求关键路径等算法的基础。

    图常用的遍历:一、深度优先搜索;二、广度优先搜索 

    深度优先搜索(遍历)步骤:(如下图)

    ① 访问起始点 v;

    ② 若v的第1个邻接点没访问过,深度遍历此邻接点;

    ③ 若当前邻接点已访问过,再找v的第2个邻接点重新遍历。

    基本思想:——仿树的先序遍历过程。

    遍历结果:v1->v2->v4->v8->v5-v3->v6->v7

    广度优先搜索(遍历)步骤:

    ① 在访问了起始点v之后,依次访问 v的邻接点;

    ② 然后再依次(顺序)访问这些点(下一层)中未被访问过的邻接点;

    ③ 直到所有顶点都被访问过为止。

    遍历结果:v1->v2->v3->v4->v5-v6->v7->v8

    4.图的连通性问题

    1)对无向图进行遍历时,对于连通图,仅需从图中任一顶点出发,进行深度优先搜索或广度优先搜索,便可访问到图中所有顶点。

    2)最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树。

    构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST性质(假设N=(V,{E})是一个连通网,U是顶点集V的一个非空子集,如果(u,v)是一条具有最小权值的边,其中u属于U,v属于V-U,则必定存在一颗包含边(u,v)的最小生成树),下面就介绍两种使用MST性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

    Kruskal算法特点:将边归并,适于求稀疏网的最小生成树。

    Prime算法特点: 将顶点归并,与边数无关,适于稠密网。

    Prime算法构造最小生成树过程如下图:

    Kruskal算法构造最小生成树过程如下图:

    5.有向无环图及其应用

    有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。

    1)拓扑排序

    拓扑排序对应施工的流程图具有特别重要的作用,它可以决定哪些子工程必须要先执行,哪些子工程要在某些工程执行后才可以执行。

    我们把顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。

    一个AOV网应该是一个有向无环图,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。

    拓扑排序的实现:

    a.在有向图中选一个没有前驱的顶点并且输出

    b.从图中删除该顶点和所有以它为尾的弧(白话就是:删除所有和它有关的边)

    c.重复上述两步,直至所有顶点输出,或者当前图中不存在无前驱的顶点为止,后者代表我们的有向图是有环的,因此,也可以通过拓扑排序来判断一个图是否有环。

    2)关键路径

    AOE-网是一个带权的有向无环图,其中,顶点表示事件,弧表示活动,权表示活动持续的时间。通常,AOE-网可用来估算工程的完成时间。

    关键路径:在AOE网中,从始点到终点具有最大路径长度(该路径上的各个活动所持续的时间之和)的路径称为关键路径。

    关键活动:关键路径上的活动称为关键活动。关键活动:e[i]=l[i]的活动

    由于AOE网中的某些活动能够同时进行,故完成整个工程所必须花费的时间应该为始点到终点的最大路径长度。关键路径长度是整个工程所需的最短工期。

    与关键活动有关的量

    (1)事件的最早发生时间ve[k]:ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的最早时间。 

    (2)事件的最迟发生时间vl[k]:vl[k]是指在不推迟整个工期的前提下,事件vk允许的最晚发生时间。

    (3)活动的最早开始时间e[i]:若活动ai是由弧<vk vj>表示,则活动ai的最早开始时间应等于事件vk的最早发生时间。因此,有:e[i]=ve[k]

    (4)活动的最晚开始时间l[i]:活动ai的最晚开始时间是指,在不推迟整个工期的前提下, ai必须开始的最晚时间。若ai由弧<vkvj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后。因此,有:l[i]=vl[j]-len<vk,vj

    示例如下:

    6.最短路径

    从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。

    1)迪杰斯塔拉算法--单源最短路径

     

    所有顶点间的最短路径—用Floyd(弗洛伊德)算法

    第八章:查找

    查找表是称为集合的数据结构。是元素间约束力最差的数据结构:元素间的关系是元素仅共在同一个集合中。(同一类型的数据元素构成的集合)

    1.静态查找表

      1)顺序查找(线性查找)

      技巧:把待查关键字key存入表头或表尾(俗称“哨兵”),这样可以加快执行速度。

    int Search_Seq( SSTable  ST , KeyType  key ){

    ST.elem[0].key =key;

    for( i=ST.length; ST.elem[ i ].key!=key;  - - i  );

          return i;

    } // Search_Seq

    //ASL=(1+n)/2,时间效率为 O(n),这是查找成功的情况:

    顺序查找的特点:

    优点:算法简单,且对顺序结构或链表结构均适用。

           缺点: ASL 太大,时间效率太低。

      2)折半查找(二分查找)——只适用于有序表,且限于顺序存储结构。

      若关键字不在表中,怎样得知并及时停止查找?

      典型标志是:当查找范围的上界≤下界时停止查找。

      ASL的含义是“平均每个数据的查找时间”,而前式是n个数据查找时间的总和,所以:

     

    3)分块查找(索引顺序查找)

    思路:先让数据分块有序,即分成若干子表,要求每个子表中的数据元素值都比后一块中的数值小(但子表内部未必有序)。然后将各子表中的最大关键字构成一个索引表,表中还要包含每个子表的起始地址(即头指针)。

    特点:块间有序,块内无序。

    查找:块间折半,块内线性

    查找步骤分两步进行:

    ① 对索引表使用折半查找法(因为索引表是有序表);

    ② 确定了待查关键字所在的子表后,在子表内采用顺序查找法(因为各子表内部是无序表);

    查找效率ASL分析:

    2.动态查找表

    1)二叉排序树和平衡二叉树

    • 二叉排序树的定义----或是一棵空树;或者是具有如下性质的非空二叉树:

     (1)若它的左子树不空,则左子树上所有结点的值均小于根的值;

     (2)若它的右子树不空,则右子树的所有结点的值均大于根的值;

     (3)它的左右子树也分别为二叉排序树。

    二叉排序树又称二叉查找树。

    二叉排序树的查找过程:

    BiTree SearchBST(BiTree T, KeyType key)

    {

            //在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素,

            //若查找成功,则返回指向该数据元素结点的指针,否则返回空指针

            if ((!T)||EQ(key, T->data.key))  return(T); //查找结束

            else if LT(key, T->data.key) return (SearchBST(T->lchild, key)); //在左子树中继续查找

            else return (SearchBST(T->rchild,key)); //在右子树中继续查找

    }

    • 二叉排序树的插入

            思路:查找不成功,生成一个新结点s,插入到二叉排序树中;查找成功则返回。

       SearchBST (K,  &t) { //K为待查关键字,t为根结点指针

       p=t;       //p为查找过程中进行扫描的指针

       while(p!=NULL)

    {

       case {

                   K= p->data:  {查找成功,return true;}

                   K< p->data :  {q=p;p=p->lchild }  //继续向左搜索

                   K> p->data :  {q=p;p=p->rchild } //继续向右搜索

                }

      }  //查找不成功则插入到二叉排序树中

    s =(BiTree)malloc(sizeof(BiTNode)); 

    s->data=K; s ->lchild=NULL; s ->rchild=NULL;

          //查找不成功,生成一个新结点s,插入到二叉排序树叶子处

    case {

                t=NULL:   t=s;   //若t为空,则插入的结点s作为根结点

                K < q->data: q->lchild=s;  //若K比叶子小,挂左边

                K > q->data: q->rchild=s; //若K比叶子大,挂右边

            }

    return OK;

    }

    • 二叉排序树的删除

    假设:*p表示被删结点的指针; PL和PR 分别表示*P的左、右孩子指针;

    *f表示*p的双亲结点指针;并假定*p是*f的左孩子;则可能有三种情况:

    *p有两颗子树,有两种解决方法:

    法1:令*p的左子树为 *f的左子树,*p的右子树接为*s的右子树;如下图(c)所示  //即 fL=PL  ;   SR=PR   ;

    法2:直接令*p的直接前驱(或直接后继)替代*p,然后再从二叉排序树中删去它的直接前驱(或直接后继) 。如图(d),当以直接前驱*s替代*p时,由于*s只有左子树SL,则在删去*s之后,只要令SL为*s的双亲*q的右子树即可。 // *s为*p左子树最右下方的结点

    删除算法如下:

    Status Delete(BiTree &p)

    {

        //从二叉排序树种删除结点p,并重接它的左或右子树

        if(!p->rchild) //右子树空,只需重接它的左子树

        {

            q=p;

            p=p->lchild;

            free(q);

        }

        else if(!p->lchild) //左子树空,只需重接它的右子树

        {

            q=p;

            p=p->rchild;

            free(q);

        }

        else //左右子树都不空

        {

            q=p; 

            s=p->lchild;

            while(s->rchild)  //转左,然后向右到尽头(找p的直接前驱) 图(b)

            {

                q=s;

                s=s->rchild;

            }

            p->data = s->data; //s指向被删结点的“前驱”

            if(q!=p)  //重接*q的右子树

            {

                q->rchild=s->lchild;

            }

            else  //q=p,说明s->rchild为空(即:p->lchild->rchild为空),重接*q的左子树

            {

                q->lchild=s->lchild;

            }

             delete s;

        }//end else 左右子树都不空

        return TRUE;

    }

    二叉排序树查找分析:和折半查找类似,与给定值比较的关键字个数不超过树的深度。然而,折半查找长度为n的表的判定树是惟一的,而含有n个结点的二叉排序树却不惟一。

    含有n个结点的二叉排序树的平均查找长度和树的形态有关。当先后插入的关键字有序时,构成的二叉排序树蜕变为单支树。树的深度为n,其平均查找长度为(n+1)/2(和顺序查找相同),这是最差的情况。最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和log2n成正比。

    (n>=2)

     

    • 平衡二叉树

    又称AVL树,即它或者是一颗空树,或者具有如下性质:它的左子树和右子树都是平衡二叉树,且左子树与右子树的深度之差的绝对值不超过1。

    平衡因子:该结点的左子树的深度减去它的右子树的深度。

    平衡二叉树的特点:任一结点的平衡因子只能取:-1、0 或 1。

    如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。我们称调整平衡过程为平衡旋转

    平衡旋转可以归纳为四类:单向右顺时针旋转(LL);单向左逆时针旋转(RR);双向旋转先左逆时针后右顺时针(LR);双向旋转先右顺时针后左逆时针(RL)

    平衡二叉树查找分析:

    时间复杂度为O(logn)

    3.B-树和B+树

    B+树是应文件系统所需而出的一种B树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:

    1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。

    2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

    3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。

    通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。

    B-树:一棵m阶的B-树或者是一棵空树,或者是满足下列要求的m叉树:

    • 树中的每个结点至多有m棵子树;

    • 若根结点不是叶子结点,则至少有两棵子树;

    • 除根结点外,所有非终端结点至少有[ m/2 ] ( 向上取整 )棵子树。

    • 所有的非终端结点中包括如下信息的数据(n,A0,K1,A1,K2,A2,….,Kn,An)

    其中:Ki(i=1,2,…,n)为关键码,且Ki < K(i+1),

    Ai 为指向子树根结点的指针(i=0,1,…,n),且指针A(i-1) 所指子树中所有结点的关键码均小于Ki (i=1,2,…,n),An 所指子树中所有结点的关键码均大于Kn。n 为关键码的个数。

    • 所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。

     

    4.哈希表

    哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

     

    1)哈希函数构造方法

    • 直接定址法

    取关键字或关键字的某个线性函数值为散列地址。

    即 H(key) = key 或 H(key) = a*key + b,其中a和b为常数。

    • 除留余数法

    取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。

    即 H(key) = key % p, p < m。

    • 数字分析法

    当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。

    仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。

    • 平方取中法

    先计算出关键字值的平方,然后取平方值中间几位作为散列地址。

    随机分布的关键字,得到的散列地址也是随机分布的。

    • 折叠法(叠加法)

    将关键字分为位数相同的几部分,然后取这几部分的叠加和(舍去进位)作为散列地址。

    用于关键字位数较多,并且关键字中每一位上数字分布大致均匀。

    • 随机数法

    选择一个随机函数,把关键字的随机函数值作为它的哈希值。

    通常当关键字的长度不等时用这种方法。

    构造哈希函数的方法很多,实际工作中要根据不同的情况选择合适的方法,总的原则是尽可能少的产生冲突

    通常考虑的因素有关键字的长度分布情况哈希值的范围等。

    如:当关键字是整数类型时就可以用除留余数法;如果关键字是小数类型,选择随机数法会比较好。

     

    2)哈希冲突的解决方法

     

    • 开放定址法

    Hi=(H(key) + di) MOD m i=1,2,…,k (k<=m)

    当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表中无待查的关键字,即查找失败。

    当冲突发生时,使用某种探查(亦称探测)技术在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到

    按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

    a.线性探查法

    hi=(h(key)+i) % m ,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1],…,直到 T[m-1],此后又循环到 T[0],T[1],…,直到探查到 有空余地址 或者到 T[d-1]为止。

    b.二次探查法

    hi=(h(key)+i*i) % m,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+1^2],T[d+2^2],T[d+3^2],…,等,直到探查到 有空余地址 或者到 T[d-1]为止。缺点是无法探查到整个散列空间。

    c.双重散列法

    hi=(h(key)+i*h1(key)) % m,0 ≤ i ≤ m-1

    基本思想是:探查时从地址 d 开始,首先探查 T[d],然后依次探查 T[d+h1(d)], T[d + 2*h1(d)],…,等。

    该方法使用了两个散列函数 h(key) 和 h1(key),故也称为双散列函数探查法。

    定义 h1(key) 的方法较多,但无论采用什么方法定义,都必须使 h1(key) 的值和 m 互素,才能使发生冲突的同义词地址均匀地分布在整个表中,否则可能造成同义词地址的循环计算。

    该方法是开放定址法中最好的方法之一。

    • 链接法(拉链法)

    将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为 m,则可将散列表定义为一个由 m 个头指针组成的指针数组 T[0..m-1] 。

    凡是散列地址为 i 的结点,均插入到以 T[i] 为头指针的单链表中。

    T 中各分量的初值均应为空指针。

    在拉链法中,装填因子 α 可以大于 1,但一般均取 α ≤ 1。

    3.哈希表的查找及其分析

    哈希表是实现关联数组(associative array)的一种数据结构,广泛应用于实现数据的快速查找。

    查找过程中,关键字的比较次数,取决于产生冲突的多少,产生的冲突少,查找效率就高,产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。

    影响产生冲突多少有以下三个因素:

    1)哈希函数是否均匀;

    2)处理冲突的方法;

    3)哈希表的加载因子。

     

    第九章:内部排序

    排序:将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。

    稳定性——若两个记录A和B的关键字值相等,且排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。

    1.插入排序

    思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。

    简言之,边插入边排序,保证子序列中随时都是排好序的。

    1)  直接插入排序

        在已形成的有序表中线性查找,并在适当位置插入,把原来位置上的元素向后顺移。

    时间效率: 因为在最坏情况下,所有元素的比较次数总和为(0+1+…+n-1)→O(n^2)。

    其他情况下也要考虑移动元素的次数。 故时间复杂度为O(n^2)

    空间效率:仅占用1个缓冲单元——O(1)

    算法的稳定性:稳定

    直接插入排序算法的实现:

    void InsertSort ( SqList &L ) 

    { //对顺序表L作直接插入排序

     for ( i = 2;  i <=L.length; i++) //假定第一个记录有序

    {

         L.r[0]= L.r[i];

           j=i-1 ;                      //先将待插入的元素放入“哨兵”位置

         while(L[0] .key<L[j].key)

        {  

            L.r[j+1]= L.r[j];

            j--  ;                    

        }      //只要子表元素比哨兵大就不断后移

        L.r[j+1]= L.r[0];      //直到子表元素小于哨兵,将哨兵值送入

                                     //当前要插入的位置(包括插入到表首)

    }

    }

    2)折半插入排序

    子表有序且为顺序存储结构,则插入时采用折半查找定可加速。

    优点:比较次数大大减少,全部元素比较次数仅为O(nlog2n)。

    时间效率:虽然比较次数大大减少,可惜移动次数并未减少, 所以排序效率仍为O(n^2) 。

    空间效率:仍为 O(1)

    稳 定  性: 稳定

     

    3)希尔排序—不稳定

    基本思想:先将整个待排记录序列分割成若干子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。

    优点:让关键字值小的元素能很快前移,且序列若基本有序时,再用直接插入排序处理,时间效率会高很多。

    时间效率:当n在某个特定范围内,希尔排序所需的比较和移动次数约为n^1.3,当n->无穷,可减少到n(log2n)^2

    空间效率:O(1)

    4)快速排序

    基本思想:从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。

    优点:因为每趟可以确定不止一个元素的位置,而且呈指数增加,所以特别快!

    前提:顺序存储结构

    时间效率:O(nlog2n) —因为每趟确定的元素呈指数增加

    空间效率:O(log2n)—因为递归要用栈(存每层low,high和pivot)

    稳 定 性: 不 稳 定 — —因为有跳跃式交换。

    算法:

    int partition(SqList &L,int low,int high)

    {

      L.r[0] = L.r[low];

      pivot key = L.r[low].key;

        while(low < high)

        {

         while(low<high&&L.r[high]>=pivot) high--;

         L.r[low] = L.r[high];

         while(low<high&&L.r[low]<=pivot) low++;

         L.r[high] = L.r[low];

        }

        L.r[low] = pivot;

        return low;

    }

     

    5)冒泡排序

    基本思路:每趟不断将记录两两比较,并按“前小后大”(或“前大后小”)规则交换。

    优点:每趟结束时,不仅能挤出一个最大值到最后面位置,还能同时部分理顺其他元素;一旦下趟没有交换发生,还可以提前结束排序。

    前提:顺序存储结构

    冒泡排序的算法分析:

    时间效率:O(n^2) —因为要考虑最坏情况

    空间效率:O(1) —只在交换时用到一个缓冲单元

    稳 定 性: 稳定  —25和25*在排序前后的次序未改变

    冒泡排序的优点:每一趟整理元素时,不仅可以完全确定一个元素的位置(挤出一个泡到表尾),还可以对前面的元素作一些整理,所以比一般的排序要快。

    选择排序:选择排序的基本思想是:每一趟在后面n-i 个待排记录中选取关键字最小的记录作为有序序列中的第i  个记录。

    6)简单选择排序

    思路异常简单:每经过一趟比较就找出一个最小值,与待排序列最前面的位置互换即可。

    ——首先,在n个记录中选择最小者放到r[1]位置;然后,从剩余的n-1个记录中选择最小者放到r[2]位置;…如此进行下去,直到全部有序为止。

    优点:实现简单

    缺点:每趟只能确定一个元素,表长为n时需要n-1趟

    前提:顺序存储结构

    时间效率:O(n^2)——虽移动次数较少,但比较次数较多

    空间效率:O(1)

    算法稳定性——不稳定

    Void SelectSort(SqList  &L ) 

    {

    for (i=1;  i<L.length; ++i)

    {

         j = SelectMinKey(L,i);  //在L.r[i..L.length]中选择key最小的记录

         if( i!=j )   r[i] <--> r[j]; //与第i个记录交换

          } //for

      }  //SelectSort

     

    7)堆排序

    设有n个元素的序列 k1,k2,…,kn,当且仅当满足下述关系之一时,称之为堆。

    如果让满足以上条件的元素序列 (k1,k2,…,kn)顺次排成一棵完全二叉树,则此树的特点是:树中所有结点的值均大于(或小于)其左右孩子,此树的根结点(即堆顶)必最大(或最小)。

    堆排序算法分析:

    时间效率:O(nlog2n)。因为整个排序过程中需要调用n-1次HeapAdjust( )算法,而算法本身耗时为log2n;

    空间效率:O(1)。仅在第二个for循环中交换记录时用到一个临时变量temp。

    稳定性: 不稳定。

    优点:对小文件效果不明显,但对大文件有效。

     

    8)归并排序----稳定

    将两个或两个以上有序表组合成一个新的有序表。

    时间复杂度:O(nlogn)

    空间复杂度:和待排记录等数量的辅助空间。

     

    9)基数排序

    时间复杂度:对于n各记录(每个记录含d个关键字,每个关键字取值范围为rd个值)进行链式基数排序的时间复杂度为O(d(n+rd)),其中每一趟分配的时间复杂度为O(n),每一趟收集的时间复杂度为O(rd)

    10)各种内部排序方法的比较讨论

    (1) 从平均时间性能看,快速排序最佳,其所需时间最省,但快速排序的最坏情况下的时间性能不如堆排序和快速排序。后两者相比较,在n较大时,归并排序所需时间较堆排序省,但它所需的辅助存储量最多。

    (2)基数排序的时间复杂度可写成O(dn)。因此,它最适用于n值很大而关键字较小的序列。

    (3)从方法的稳定性来比较,基数排序是稳定的内排方法,所需时间复杂度为O(n^2)的简单排序方法也是稳定的,然而,快速排序、堆排序和希尔排序等时间性能较好的排序方法是不稳定的。

    展开全文
  • 超硬核!数据结构学霸笔记,考试面试吹牛就靠它

    万次阅读 多人点赞 2021-03-26 11:11:21
    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    上次发操作系统笔记,很快浏览上万,这次数据结构比上次硬核的多哦,同样的会发超硬核代码,关注吧。

    超硬核!操作系统学霸笔记,考试复习面试全靠它

     


     


     

    第一次笔记(复习c,课程概述)

    第一节课复习了c语言的一些知识,并简单介绍了数据结构这门课程。

     

    1、引用和函数调用:

    1.1引用:对一个数据建立一个“引用”,他的作用是为一个变量起一个别名。这是C++对C语言的一个重要补充。

    用法很简单:

    int a = 5;

    int &b = a;

    b是a别名,b与a代表的是同一个变量,占内存中同一个存储单元,具有同一地址。

    注意事项:

    1. 声明一个引用,同时必须初始化,及声明它代表哪一个变量。(作为函数参数时不需要初始化)
       
    2. 在声明一个引用后,不能再作为另一变量的引用。

         3。不能建立引用数组。

    1.2函数调用:

    其实还是通过函数来理解了一下引用

    void Myswap1(int a,int b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap2(int &a,int &b)
    {
    	int c = a;
    	a = b;
    	b = c;
    }
    
    void Myswap3(int *pa,int *pb)
    {
    	int c = *pa;
    	*pa = *pb;
    	*pb = c;
    }

    这三个函数很简单,第一个只传了值进来,不改变全局变量;而第三个也很熟悉,就是传递了地址,方便操作。依旧是“值传递”的方式,只不过传递的是变量的地址而已;那二就彻底改变了这些东西,引用作为函数参数,传入的实参就是变量,而不是数值,真正意义上的“变量传递”。

     

    2、数组和指针:

    这一块讲得比较简单,就是基本知识。

    主要内容:

    1、函数传数组就是传了个指针,这个大家都知道,所以传的时候你写arr[],里面写多少,或者不写,都是没关系的,那你后面一定要放一个变量来把数组长度传进来。

    2、还有就是,定义:int arr[5],你访问越界是不会报错的,但是逻辑上肯定就没有道理了。那用typedef int INTARR[3];访问越界,在vs上会报错,要注意。

    3、再说一下指针和数组名字到底有什么区别?这本来就是两个东西,可能大家没注意罢了。

    第一:指针可以自增,数组名不行,因为是常量啊。

    第二:地址不同,虽然名字[n],都可以这样用,但是数组名地址就是第一个元素地址。指针地址就是那个指针的地址,指针里存的才是第一个元素地址。

    第三:sizeof(),空间不一样,数组是占数组那么大空间。指针是四个字节。

    本来就是俩东西,这么多不同都是本质不同的体现。

    3、结构体:

    也是讲的基本操作,基本就是这个东西:

    typedef struct Date
    {
    	int Year;
    	int Month;
    	int Day;
    	struct Date *pDate;
    }Date, *pDate;

    1、内部无指向自己的指针才可以第一行先不起名字。

    2、内部不能定义自己的,如果能的话那空间不就无限了么。很简单的逻辑

     

    指针我不习惯,还是写Date *比较顺眼

    3、有同学没注意:访问结构体里的东西怎么访问?

    Date.这种形式,或者有指向这个节点的指针p可以p->这种形式,别写错了。

     

    4、还有就是结构体能直接这么赋值:

       Date d1 = {2018,9,11};

    我竟然不知道,以前还傻乎乎的一个一个赋值呢。

     

    5、还有,想写一下结构体有什么优点。。

    这一块可能写的就不好了,因为不是老师讲的。。

    比如学生成绩,如果不用结构体,我们一个学生可能有十几个信息,那定义变量和操作就很烦琐了,数据结构有一种松散的感觉。用一个结构体来表示更好,无论是程序的可读性还是可移植性还是可维护性,都得到提升。还有就是函数调用的时候,传入相当多的参数,又想操作或者返回,那是很麻烦的事。现在只传入一个结构体就好了,操作极其简单。总结一下就是好操作,中看中用,有机地组织了对象的属性。以修改结构体成员变量的方法代替了函数(入口参数)的重新定义。

    基本就这样吧。

    6、还有就是它了:typedef int INTARR[3];这样直接定义了一个数据类型,长度为3的数组,然后直接这样用就可以了:

    INTARR arr1;

     

    回忆完C语言储备知识,然后讲了数据结构的基本概念

     

    数据结构是一门研究非数值计算的程序设计问题中计算机的操作对象以及他们之间的关系和操作等的学科。

    数据:是对客观事物的符号表示,在计算机中指能输入到计算机中并被处理的符号总称。

    数据元素:数据的基本单位

    数据项:数据的不可分割的最小单位

    数据对象:性质相同的数据元素的集合。

    举例:动物是数据,某只动物是数据元素,猫狗是数据对象,颜色可以是数据项。

     

    数据元素之间存在某种关系,这种关系成为结构。

    四种基本结构:

    集合:除了同属一个集合无其他关系。

    线性结构:一对一的关系

    树形结构:一对多的关系

    图状结构:多对多的关系

     

    第二次笔记(基本概念,时间空间复杂度)

    今天继续说明了一些基本概念,讲解了时间空间复杂度。

    (对于概念的掌握也很重要)

     

    元素之间的关系在计算机中有两种表示方法:顺序映像和非顺序映像,由此得到两种不同的储存结构:

    顺序存储结构和链式存储结构。

     

    顺序:根据元素在存储器中的相对位置表示关系

    链式:借助指针表示关系

     

    数据类型:是一个值的集合和定义在这个值集上的一组操作的总称。

    抽象数据类型:是指一个数学模型以及定义在该模型上的一组操作。(仅仅取决于逻辑特性,与其在计算机内部如何表示和实现无关)

     

    定义抽象数据类型的一种格式:

    ADT name{

    数据对象:<>

    数据关系:<>

    基本操作:<>

    }ADT name

     

    算法:是对特定问题求解步骤的一种描述。

    算法五个特性:

    1. 有穷性:有穷的时间内完成,或者可以说是可接受的时间完成
    2. 确定性:对于相同的输入只能得到相同的输出
    3. 可行性:描述的操作都可以执行基本操作有限次来实现
    4. 输入:零个或多个输入。取自于某个特定对象的集合
    5. 输出:一个或多个输出

     

    设计要求:正确性、可读性、健壮性、效率与低存储量需求。

    执行频度概念:是指通过统计算法的每一条指令的执行次数得到的。

    执行频度=算法中每一条语句执行次数的和

    一般认定每条语句执行一次所需时间为单位时间(常数时间)O(1)

     

    几个小知识和小问题:

    1)循环执行次数n+1次,不是n次。第一次执行i=1和判断i<=n以后执行n次判断和i++。所以该语句执行了n+1次。在他的控制下,循环体执行了n次。

    2)四个并列程序段:分别为O(N),O(N^2),O(N^3),O(N*logN),整个程序时间复杂度为O(N^3),因为随着N的增长,其它项都会忽略

    3)算法分析的目的是分析算法的效率以求改进

    4)对算法分析的前提是算法必须正确

    5)原地工作指的不是不需要空间,而是空间复杂度O(1),可能会需要有限几个变量。

    实现统一功能两种算法:时间O(2^N),O(N^10),假设计算机可以运行10^7秒,每秒可执行基本操作10^5次,问可解问题规模各为多少?选哪个更为合适?

    计算机一共可执行10^7*10^5=10^12次

    第一种:n=log2,(10^12)=12log(2,10)

    第二种:n=(10^12)^0.1

    显然1更适用。

    虽然一般情况多项式算法比指数阶更优

     

    时间空间复杂度概述

     

    找个时间写一写时间复杂度和一些问题分类,也普及一下这方面知识。

    如何衡量一个算法好坏

    很显然,最重要的两个指标:需要多久可以解决问题、解决问题耗费了多少资源

    那我们首先说第一个问题,要多长时间来解决某个问题。那我们可以在电脑上真实的测试一下嘛,多种方法比一比,用时最少的就是最优的啦。

    但是没必要,我们可以通过分析计算来确定一个方法的好坏,用O()表示,括号内填入N、1,等式子。

    这到底是什么意思呢?

    简单来说,就是这个方法,时间随着数据规模变化而增加的快慢。时间可以当成Y,数据规模是X,y=f(x),就这样而已。但是f(x)不是准确的,只是一个大致关系,y=10x,我们也视作x,因为他的增长速度还是n级别的。现在就可以理解了:一般O(N)就是对每个对象访问优先次数而已。请注意:O(1)它不是每个元素访问一次,而是Y=1的感觉,y不随x变化而变化,数据多大它的时间是不变的,有限的常数操作即可完成。

    那我们就引入正规概念:

    时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。

    计算机科学中,算法的时间复杂度是一个函数,它定性描述了该算法的运行时间。这是一个关于代表算法输入值的字符串的长度的函数。时间复杂度常用大O符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,它考察当输入值大小趋近无穷时的情况。

    注意:文中提到:不包括这个函数的低阶项和首项系数。什么意思呢?就是说10n,100n,哪怕1000000000n,还是算做O(N),而低阶项是什么意思?不知大家有没有学高等数学1,里面有最高阶无穷大,就是这个意思。举个例子。比如y=n*n*n+n*n+n+1000

    就算做o(n*n*n),因为增长速率最大,N*N及其它项增长速率慢,是低阶无穷大,n无限大时,忽略不计。

     

    那接着写:o(n*n*n)的算法一定不如o(n)的算法吗?也不一定,因为之前说了,时间复杂度忽略了系数,什么意思?o(n)可以是10000000n,当n很小的时候,前者明显占优。

    所以算法要视实际情况而定。

    算法的时间 复杂度常见的有:
    常数阶 O(1),对数阶 O(log n),线性阶 O(n),
    线性对数阶 O(nlog n),平方阶 O(n^2),立方阶 O(n^3),…,
    k 次方阶O(n^k),指数阶 O(2^n),阶乘阶 O(n!)。

    常见的算法的时间 复杂度之间的关系为:
           O(1)<O(log n)<O(n)<O(nlog n)<O(n^2)<O(2^n)<O(n!)<O(n^n) 

     

    我们在竞赛当中,看见一道题,第一件事就应该是根据数据量估计时间复杂度。

    计算机计算速度可以视作10^9,如果数据量是10000,你的算法是O(N*N),那就很玄,10000*10000=10000 0000,别忘了还有常数项,这种算法只有操作比较简单才可能通过。你可以想一想O(nlog n)的算法一般就比较稳了。那数据量1000,一般O(N*N)就差不多了,数据量更小就可以用复杂度更高的算法。大概就这样估算。

     

    当 n 很大时,指数阶算法和多项式阶算法在所需时间上非常
    悬殊。因此,只要有人能将现有指数阶算法中的任何一个算法化
    简为多项式阶算法,那就取得了一个伟大的成就。

    体会一下:

     

    空间复杂度也是一样,用来描述占空间的多少。

    注意时间空间都不能炸。

    所以才发明了那么多算法。

    符上排序算法的时间空间表,体会一下:

     


    排序博客:加深对时间空间复杂度理解

    https://blog.csdn.net/hebtu666/article/details/81434236

     

     

     

    引入:算法优化

     

    想写一系列文章,总结一些题目,看看解决问题、优化方法的过程到底是什么样子的。

     

    系列问题一:斐波那契数列问题

    在数学上,斐波纳契数列以如下被以递归的方法定义:F(0)=0,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)根据定义,前十项为1, 1, 2, 3, 5, 8, 13, 21, 34, 55

    问题一:

    给定一个正整数n,求出斐波那契数列第n项(这时n较小)

    解法一:最笨,效率最低,暴力递归,完全是抄定义。请看代码

    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)

     

    分析一下,为什么说递归效率很低呢?咱们来试着运行一下就知道了

     

    比如想求f(10),计算机里怎么运行的?

     

    每次要计算的函数量都是指数型增长,时间复杂度O(2^(N/2))<=T(N)<=O(2^N),效率很低。效率低的原因是,进行了大量重复计算,比如图中的f(8),f(7).....等等,你会发现f(8)其实早就算过了,但是你后来又要算一遍。

    如果我们把计算的结果全都保存下来,按照一定的顺序推出n项,就可以提升效率, 斐波那契(所有的一维)的顺序已经很明显了,就是依次往下求。。

    优化1

    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]

     

    时间复杂度o(n),依次往下打表就行,空间o(n).

    继续优化:既然只求第n项,而斐波那契又严格依赖前两项,那我们何必记录那么多值呢?记录前两项就行了

     

    优化2

    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    此次优化做到了时间o(n),空间o(1)

    附:这道题掌握到这里就可以了,但是其实有时间o(log2n)的方法

     

    优化三:

    学习过线性代数的同学们能够理解:

     

    结合快速幂算法,我们可以在o(log2n)内求出某个对象的n次幂。(在其它专题详细讲解)

    注意:只有递归式不变,才可以用矩阵+快速幂的方法。

    注:优化三可能只有在面试上或竞赛中用,当然,快速幂还是要掌握的。

     

    经过这个最简单的斐波那契,大家有没有一点感觉了?

    好,我们继续往下走

    (补充:pat、蓝桥杯原题,让求到一万多位,结果模一个数。

    可以每个结果都对这个数取模,否则爆内存,另:对于有多组输入并且所求结果类似的题,可以先求出所有结果存起来,然后直接接受每一个元素,在表中查找相应答案)

     

    问题二:

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    依旧是找递推关系,分析:跳一阶,就一种方法,跳两阶,它可以一次跳两个,也可以一个一个跳,所以有两种,三个及三个以上,假设为n阶,青蛙可以是跳一阶来到这里,或者跳两阶来到这里,只有这两种方法。它跳一阶来到这里,说明它上一次跳到n-1阶,同理,它也可以从n-2跳过来,f(n)为跳到n的方法数,所以,f(n)=f(n-1)+f(n-2)

    和上题的优化二类似。

    因为递推式没变过,所以可以用优化三

     

    问题三:

    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?提示,大矩形看做是长度吧

    请读者自己先思考一下吧。。。仔细看题。。仔细思考

    如果n是1,只有一种,竖着放呗;n是2,两种:

    n等于3,三种:

     

    题意应该理解了吧?

    读到这里,你们应该能很快想到,依旧是斐波那契式递归啊。

    对于n>=3:怎么能覆盖到三?只有两种办法,从n-1的地方竖着放了一块,或者从n-2的位置横着放了两块呗。

    和问题二的代码都不用变。

     

    问题四:

    给定一个由0-9组成的字符串,1可以转化成A,2可以转化成B。依此类推。。25可以转化成Y,26可以转化成z,给一个字符串,返回能转化的字母串的有几种?

    比如:123,可以转化成

    1 2 3变成ABC,

    12 3变成LC,

    1 23变成AW,三种,返回三,

    99999,就一种:iiiii,返回一。

    分析:求i位置及之前字符能转化多少种。

    两种转化方法,一,字符i自己转换成自己对应的字母,二,和前面那个数组成两位数,然后转换成对应的字母

    假设遍历到i位置,判断i-1位置和i位置组成的两位数是否大于26,大于就没有第二种方法,f(i)=f(i-1),想反,等于f(i-1)+f(i-2)

    注意:递归式不确定,不能用矩阵快速幂

     

    (这几个题放到这里就是为了锻炼找递归关系的能力,不要只会那个烂大街的斐波那契)

     

    '''
    斐波那契问题:
    斐波纳契数列以如下被以递归的方法定义:
    F(1)=1
    F(2)=1
    F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)
    '''
    #暴力抄定义,过多重复计算
    def f0(n):
        if n==1 or n==2:
            return 1
        return f(n-1)+f(n-2)
    #记录结果后依次递推的优化,时间O(N)
    def f1(n):
        if n==1 or n==2:
            return 1
        l=[0]*n
        l[0],l[1]=1,1
        for i in range(2,n):
            l[i]=l[i-1]+l[i-2]
        return l[-1]
    #既然严格依赖前两项,不必记录每一个结果,优化空间到O(1)
    def f2(n):
        a,b=1,1
        for i in range(n-1):
            a,b=b,a+b
        return a

    第三次笔记(线性表结构和顺序表示)

     

    这节课介绍了线性表结构和顺序表示的一部分内容。

    操作太多,而且书上有,就不一一介绍分析了。

    线性表定义:n个数据元素的有限序列。

    特点:

    1. 存在唯一一个称作“第一个”的元素。
    2. 存在唯一一个称作“最后一个”的元素
    3. 除最后一个元素外,集合中每一个元素都只有一个直接前趋
    4. 除最后一个元素外,集合中每一个元素都只有一个直接后继

    注意1、2条:证明循环的序列不是线性表

     

    注意:

    1)线性表从1开始,线性表第一个元素对应到数组中下标是0.

    2)函数通过引用对线性表的元素进行修改即可

    3)数组比较特别,它即可视为逻辑结构,又可视为存储结构。

    4)每一个表元素都是不可再分的原子数据。一维数组可以视为线性表,二维数组不可以,在逻辑上它最多可以有两个直接前趋和直接后继。

    5)线性表具有逻辑上的顺序性,在序列中各元素有其先后次序,各个数据元素在线性表中的逻辑位置只取决于序号。

     

    顺序表:是线性表的循序储存结构,以物理位置表示逻辑关系,任意元素可以随机存取。用一组地址连续的存储单元依次存储线性表中的元素。逻辑顺序和物理顺序是一致的。可以顺序访问,也可随机访问。

    顺序表存储:

    这些定式还是很重要的,比如define typedef等,真正实现时最好就这样写,不要自己规定个长度和数据类型,这样以后好维护、修改。

    静态存储分配:

    #define maxSize 100//显式定义表长

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType data[maxSize];//静态分配存储表元素向量

    Int n;//实际表中个数

    }SeqList;

     

    动态存储分配:

    #define maxSize 100//长度初始定义

    Typedef int DataType;//定义数据类型

    Typedef struct{

    DataType *data;//动态分配数组指针

    Int maxSize,n;//最大容量和当前个数

    }SeqList;

     

    初始动态分配:

    Data=(DataType *)malloc(sizeof(DataType)* initSize);

    C++:data=new DataType[initSize];

    maxSize=initSize;n=0;

    动态分配存储,向量的存储空间是在程序执行过程中通过动态存储分配来获取的。空间满了就另外分配一块新的更大的空间,用来代替原来的存储空间,从而达到扩充的目的。

     

    插入:需要查找,移动元素,概率上1,2,3....n,平均O(N)

    删除:同样需要移动元素。填充被空出来的存储单元。

    在等概率下平均移动次数分别为n/2,(n-1)/2

     插入注意事项:

    1. 需要判断是否已满
    2. 要从后向前移动,否则会冲掉元素

    删除注意事项:

    1. 需要先判断是否已空
    2. 需要把后方元素前移,要从前向后。

     

    注意:线性表的顺序存储借用了一维数组,但是二者不同:

    1. 一维数组各非空结点可以不相继存放,但顺序表是相继存放的
    2. 顺序表长度是可变的,一维数组长度是确定的,一旦分配就不可变
    3. 一维数组只能按下标存取元素,顺序表可以有线性表的所有操作。

     

     

    第四次笔记(链表概述)

     

    介绍了链表和基本操作

    用一组物理位置任意的存储单元来存放线性表的数据元素。 这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任意位置上的。因此,链表中元素的逻辑次序和 物理次序不一定相同。

     

    定义:

    typedef  struct  Lnode{  
            //声明结点的类型和指向结点的指针类型  
            ElemType         data;    //数据元素的类型 
            struct   Lnode  *next;   //指示结点地址的指针  
    }Lnode, *LinkList;  
    struct Student
    { char num[8];   //数据域
      char name[8];  //数据域
      int score;          //数据域
      struct Student *next;  // next 既是 struct Student 
                                           // 类型中的一个成员,又指 
                                           // 向 struct Student 类型的数据。 
    }Stu_1, Stu_2, Stu_3, *LL;  
    

    头结点:在单链表的第一个结点之前人为地附设的一个结点。

    带头结点操作会方便很多。

    带和不带的我都写过了

    下面列出我见过的一些好题

    1、

    线性表的每个结点只能是一个简单类型,而链表的每个结点可以是一个复杂类型。

    • 正确
    • 错误

     

    错,线性表是逻辑结构概念,可以顺序存储或链式储,与元素数据类型无关。链表就是线性表的一种  前后矛盾

     

    2、

    若某线性表中最常用的操作是在最后一个元素之后插入一个元素和删除第一个元素,则采用(    )存储方式最节省运算时间。

    • 单链表
    • 仅有头指针的单循环链表
    • 双链表
    • 仅有尾指针的单循环链表

      对于A,B,C要想在尾端插入结点,需要遍历整个链表。

      对于D,要插入结点,只要改变一下指针即可,要删除头结点,只要删除指针.next的元素即可。

     

    3、注意:线性表是具有n个数据元素的有限序列,而不是数据项

     

    4、

    以下关于单向链表说法正确的是

    • 如果两个单向链表相交,那他们的尾结点一定相同
    • 快慢指针是判断一个单向链表有没有环的一种方法
    • 有环的单向链表跟无环的单向链表不可能相交
    • 如果两个单向链表相交,那这两个链表都一定不存在环

    自己多画画想想就明白了,关于快慢指针我以后会写总结。

     

    5、

    链接线性表是顺序存取的线性表 。 ( )

    • 正确
    • 错误

    链接线性表可以理解为链表
    线性表分为顺序表和链表
    顺序表是顺序存储结构、随机存取结构
    链表是随机存储结构、顺序存取结构

     

    6、

    typedef struct node_s{
        int item;
        struct node_s* next;
    }node_t;
    node_t* reverse_list(node_t* head)
    {
        node_t* n=head;
        head=NULL;
        while(n){
        _________               
        }
        return head;
     }

    空白处填入以下哪项能实现该函数的功能?

    • node_t* m=head; head=n; head->next=m; n=n->next;
    • node_t* m=n; n=n->next; m->next=head; head=m;
    • node_t* m=n->next; n->next=head; n=m; head=n;
    • head=n->next; head->next=n; n=n->next;


     

    代码的功能是要实现链表的反转。为了方便阐述,每个结点用①②③④⑤⑥等来标示。

    在执行while(n)循环之前,有两句代码:

    node_t* n=head;

    head=NULL;

    这两行代码的中:第一句的作用是用一个临时变量n来保存结点①,第二句是把head修改为NULL。

    然后就开始遍历了,我们看到while循环里的那四句代码:

    node_t* m=n; 
    n=n->next; 
    m->next=head; 
    head=m;
    

    先看前两句,用m来保存n,然后让n指向n的下一个结点,之所以复制 n 给 m ,是因为 n 的作用其实是  控制while循环次数  的作用,每循环一次它就要被修改为指向下一个结点。

    再看后两句,变量head在这里像是一个临时变量,后两句让 m 指向了 head,然后 head 等于 m。

     

    7、

    若某表最常用的操作是在最后一个结点之后插入一个节点或删除最后一二个结点,则采用()省运算时间。

    • 单链表
    • 双链表
    • 单循环链表
    • 带头结点的双循环链表

    D

    带头结点的双向循环链表,头结点的前驱即可找到最后一个结点,可以快速插入,再向前可以找到最后一二个结点快速删除

    单链表找到链表尾部需要扫描整个链表

    双链表找到链表尾部也需要扫描整个链表

    单循环链表只有单向指针,找到链表尾部也需要扫描整个链表

     

    8、

    单链表的存储密度(  )

    • 大于1
    • 等于1
    • 小于1
    • 不能确定

    全麦白

    存储密度 = 数据项所占空间 / 结点所占空间

     

    9、完成在双循环链表结点p之后插入s的操作是

    • s->prior=p; s->next=p->next; p->next->prior=s ; p->next=s;

     

    10、用不带头结点的单链表存储队列,其队头指针指向队头结点,队尾指针指向队尾结点,则在进行出队操作时()

    • 仅修改队头指针
    • 仅修改队尾指针
    • 队头、队尾指针都可能要修改
    • 队头、队尾指针都要修改

     

    当只有一个元素,出队列时,要将队头和队尾,指向-1.所以说队头和队尾都需要修改

     

     

    链表刷了几百道吧,好题还有很多,以后接着更新

     

     

    第六次笔记(链表选讲/静态链表)

     

    本节课介绍了单链表的操作实现细节,介绍了静态链表。

     

    链表带头的作用:对链表进行操作时,可以对空表、非空表的情况以及 对首元结点进行统一处理,编程更方便。

    下面给出带头的单链表实现思路:

     

    按下标查找:

    判断非法输入,当 1 < =i <= n 时,i 的值是合法的。

    p = L -> next; j = 1;

    while ( p && j < i ) {  p = p ->next; ++j; }

    return 

     

    按值查找:

     p = L1 -> next;

     while ( p && p ->data!=key)          p = p -> next;

    return;

     

    插入:

    判断

    查找

    创建

    插入

     

    删除:

    查找

    删除

    释放内存

     

    静态链表

    对于线性链表,也可用一维数组来进行描述。这种描述方法便于在没有指针类型的高级程序设计语言中使用链表结构。

    这种存储结构,仍需要预先分配一个较大的空间,但在作为线性表的插入和删除操作时不需移动元素,仅需修改指针,故仍具有链式存储结构的主要优点。

     

    表示:

    #define MAXSIZE 1000      / /链表的最大长度

    typedef  struct{      

        ElemType data;        

        int cur;

    }component,  SLinkList[MAXSIZE];

     

    过程:

     

     

    顺序存储线性表实现

     

    在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构。

     

    顺序存储结构的主要优点是节省存储空间,因为分配给数据的存储单元全用存放结点的数据(不考虑c/c++语言中数组需指定大小的情况),结点之间的逻辑关系没有占用额外的存储空间。采用这种方法时,可实现对结点的随机存取,即每一个结点对应一个序号,由该序号可以直接计算出来结点的存储地址。但顺序存储方法的主要缺点是不便于修改,对结点的插入、删除运算时,可能要移动一系列的结点。

    优点:随机存取表中元素。缺点:插入和删除操作需要移动元素。

     

    线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。

    给出两种基本实现:

    /*
    静态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INITSIZE 100
    #define ElemType int
    #define Status int
    #define OK     1
    #define ERROR  0
    
    typedef struct
    {
    	ElemType elem[LIST_INITSIZE];
    	int length;
    }SqList;
    
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DisCreat(SqList A,SqList *B,SqList *C);//拆分(按正负),也可以根据需求改
    //虽然思想略简单,但是要写的没有错误,还是需要锻炼coding能力的
    
    Status InitList(SqList *L)
    {
        L->length = 0;//长度为0
        return OK;
    }
    
    Status ListInsert(SqList *L, int i,ElemType e)
    {
        int j;
        if(i<1 || i>L->length+1)
            return ERROR;//判断非法输入
        if(L->length == LIST_INITSIZE)//判满
        {
            printf("表已满");//提示
            return ERROR;//返回失败
        }
        for(j = L->length;j > i-1;j--)//从后往前覆盖,注意i是从1开始
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        (L->length)++;//表长加1
        return OK;//反回成功
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i = 0;i < L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DisCreat(SqList A,SqList *B,SqList *C)
    {
        int i;
        for(i = 0;i < A.length;i++)//依次遍历A中元素
        {
            if(A.elem[i]<0)//判断
                ListInsert(B,B->length+1,A.elem[i]);//直接调用插入函数实现尾插
            else
                ListInsert(C,C->length+1,A.elem[i]);
        }
    }
    
    int main(void)
    {
        //复制的
    	SqList L;
    	SqList B, C;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,33,-3,-88,21,77,0,-9};
    	InitList(&L);
    	InitList(&B);
    	InitList(&C);
    	for (i = 1; i <= 9; i++)
    		ListInsert(&L,i,data[i-1]);
        printf("插入完成后L = : ");
    	ListPrint(L);
        ListDelete(&L,1,&e);
    	printf("删除第1个后L = : ");
    	ListPrint(L);
        DisCreat(L , &B, &C);
    	printf("拆分L后B = : ");
    	ListPrint(B);
    	printf("拆分L后C = : ");
    	ListPrint(C);
    	printf("拆分L后L = : ");
    	ListPrint(L);
    }

    静态:长度固定

    动态:不够存放可以加空间(搬家)

     

    /*
    子任务名任务:1_2 动态顺序存储线性表的基本实现
    */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define LIST_INIT_SIZE 100
    #define LISTINCREMENT 10
    #define Status int
    #define OVERFLOW -1
    #define OK 1
    #define ERROR 0
    #define ElemType int
    
    typedef struct
    {
    	ElemType * elem;
    	int length;
    	int listsize;
    }SqList;
    //函数介绍
    Status InitList(SqList *L); //初始化
    Status ListInsert(SqList *L, int i,ElemType e);//插入
    Status ListDelete(SqList *L,int i,ElemType *e);//删除
    void ListPrint(SqList L);//输出打印
    void DeleteMin(SqList *L);//删除最小
    
    Status InitList(SqList *L)
    {
        L->elem = (ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));//申请100空间
    	if(!L->elem)//申请失败
    		return ERROR;
    	L->length = 0;//长度0
    	L->listsize = LIST_INIT_SIZE;//容量100
    	return OK;//申请成功
    }
    
    Status ListInsert(SqList *L,int i,ElemType e)
    {
        int j;
        ElemType *newbase;
        if(i<1 || i>L->length+1)
            return ERROR;//非法输入
            
        if(L->length >= L->listsize)//存满了,需要更大空间
        {
            newbase = (ElemType*)realloc(L->elem,(L->listsize+LISTINCREMENT)*sizeof(ElemType));//大10的空间
            if(!newbase)//申请失败
                return ERROR;
            L->elem = newbase;//调指针
            L->listsize+= LISTINCREMENT;//新容量
        }
        
        for(j=L->length;j>i-1;j--)//从后往前覆盖
            L->elem[j] = L->elem[j-1];
        L->elem[i-1] = e;//在留出的位置赋值
        L->length++;//长度+1
        return OK;
    }
    
    Status ListDelete(SqList *L,int i,ElemType *e)
    {
        int j;
        if(i<1 || i>L->length)//非法输入/表空
            return ERROR;
        *e = L->elem[i-1];//为了返回值
        for(j = i-1;j <= L->length;j++)//从前往后覆盖
            L->elem[j] = L->elem[j+1];
        (L->length)--;//长度减1
        return OK;//返回删除值
    }
    
    void ListPrint(SqList L)
    {
        int i;
        for(i=0;i<L.length;i++)
            printf("%d ",L.elem[i]);
        printf("\n");//为了美观
    }
    
    void DeleteMin(SqList *L)
    {
        //表空在Listdelete函数里判断
        int i;
        int j=0;//最小值下标
        ElemType *e;
        for(i=0;i<L->length;i++)//寻找最小
        {
            if(L->elem[i] < L->elem[j])
                j=i;
        }
        ListDelete(L,j+1,&e);//调用删除,注意j要+1
    }
    
    int main(void)
    {
    	SqList L;
    	int i;
    	ElemType e;
    	ElemType data[9] = {11,-22,-33,3,-88,21,77,0,-9};
    	InitList(&L);
    	for (i = 1; i <= 9; i++)
    	{
    		ListInsert(&L,i,data[i-1]);
    	}
    	printf("插入完成后 L = : ");
    	ListPrint(L);
        ListDelete(&L, 2, &e);
    	printf("删除第 2 个后L = : ");
    	ListPrint(L);
        DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    	DeleteMin(&L);
    	printf("删除L中最小值后L = : ");
    	ListPrint(L);
    }

    单链表,不带头实现

    链表是一种物理存储单元上非连续、非顺序的存储结构数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

    使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。链表有很多种不同的类型:单向链表双向链表以及循环链表

     

    下面给出不带头的单链表标准实现:

    定义节点:

    typedef struct node 
    { 
        int data;
        struct node * next;
    }Node;

    尾插:

    void pushBackList(Node ** list, int data) 
    { 
        Node * head = *list;
        Node * newNode = (Node *)malloc(sizeof(Node));//申请空间
        newNode->data = data; newNode->next = NULL;
        if(*list == NULL)//为空
            *list = newNode;
        else//非空
        {
            while(head ->next != NULL)
                head = head->next;
            head->next = newNode;
        }
    }
    
    

    插入:

    int insertList(Node ** list, int index, int data) 
    {
        int n;
        int size = sizeList(*list); 
        Node * head = *list; 
        Node * newNode, * temp;
        if(index<0 || index>size) return 0;//非法
        newNode = (Node *)malloc(sizeof(Node)); //创建新节点
        newNode->data = data; 
        newNode->next = NULL;
        if(index == 0) //头插
        {
            newNode->next = head; 
            *list = newNode; 
            return 1; 
        }
        for(n=1; n<index; n++) //非头插
            head = head->next;
        if(index != size) 
            newNode->next = head->next; 
        //链表尾部next不需指定
        head->next = newNode; 
        return 1;
    }
    

    按值删除:

    void deleteList(Node ** list, int data) 
    { 
        Node * head = *list; Node * temp; 
        while(head->next!=NULL) 
        { 
            if(head->next->data != data) 
            { 
                head=head->next; 
                continue; 
            } 
            temp = head->next;
            if(head->next->next == NULL) //尾节点删除
                head->next = NULL; 
            else 
                head->next = temp->next; 
            free(temp);
        }    
        head = *list; 
        if(head->data == data) //头结点删除
        { 
            temp = head; 
            *list = head->next; 
            head = head->next; 
            free(temp); 
        }
    }
    

    打印:

    void printList(Node * head) 
    { 
        Node * temp = head; 
        for(; temp != NULL; temp=temp->next) 
            printf("%d ", temp->data); 
        printf("\n"); 
    }

    清空:

    void freeList(Node ** list) 
    { 
        Node * head = *list; 
        Node * temp = NULL; 
        while(head != NULL) //依次释放
        { 
            temp = head; 
            head = head->next; 
            free(temp); 
        } 
        *list = NULL; //置空
    }

    别的也没啥了,都是基本操作

    有些代码要分情况,很麻烦,可读性较强吧

    看我压缩代码:https://blog.csdn.net/hebtu666/article/details/81261043

     

    双链表带头实现

    以前写的不带头的单链表实现,当时也啥也没学,好多东西不知道,加上一心想压缩代码,减少情况,所以写得不太好。

    请教了老师,首先是命名问题和代码紧凑性等的改进。还有可读性方面的改进,多写了一些注释。并且因为带头的比较好写,好操作,所以标准写法也不是很长,繁琐。

     

     

    下面贴代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef struct node{
        int key;//数据
        struct node * prev;//前驱
        struct node * next;//后继
    }Node;

    初始化(带头) 

    Node * list;
    //初始化,这里·我们list不再是NULL,而是指向了一个节点
    //这个改进方便了很多操作,也不用借助二重指针把list和next统一表示了
    
    void init(Node * list)//初始化
    {
        list = (Node *)malloc(sizeof(Node));
        list->next = NULL;
        list->prev = NULL;
    }

    查找(不用再判断一下空不空)

    Node * find(int key,Node * list)
    {
        Node * head = list->next;//从头节点后面开始找
        while(head != NULL && head->key != key)//找到或空跳出
            head = head->next;
        return head;
    }

    打印

    void printList(Node * list)//打印
    {
        Node * temp = list->next;头节点下一个开始
        while(temp != NULL)
        {
            printf("%d ",temp->key);
            temp = temp->next;
        }
        printf("\n");
    }

    删除指定结点

    void delete(Node * list)//删除指定结点
    {
        list->prev->next = list->next;前面后面指针改了,再free自己即可
        list->next->prev = list->prev;
        free(list);
    }

    配合一下删除:

    void deleteKey(int key,Node * list)
    {
        delete(find(key,list));
    }

    头插:

    void insertHead(int key,Node * list)//头插
    {
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        newNode->next = list->next;//赋值后继
        if(list->next != NULL)//如果有后继,赋值后继的前指针为新结点
            list->next->prev = newNode;
        list->next = newNode;//改头
        newNode->prev = list;//赋值新结点前指针
    }

    按下标插入

    单链表都写了,我就不写长度函数和判断非法了,用的时候注意吧。

    void insert(int key,Node * list,int index)
    {
        Node * head=list;//最后指到要插位置的前一个即可
        Node * newNode = (Node *)malloc(sizeof(Node));//初始化
        newNode->key = key;
        while(index--)
            head = head->next;
        newNode->next = head->next;//修改指针
        newNode->prev = head;
        head->next = newNode;
    }

    指定某值后插入不写了,和这个修改指针逻辑一样,再传一个find配合一下就行了。

     

    然后简单测试

    int main()
    {
        Node * list = NULL;
        init(list);
        insertHead(1,list);
        insertHead(2,list);
        insertHead(3,list);
        printList(list);
        deleteKey(2,list);
        printList(list);
        insert(10,list,0);
        printList(list);
    }

     

    第七次笔记(栈/队列)

     

    介绍栈和队列基本概念和用法。

     

    设输入序列1、2、3、4,则下述序列中( )不可能是出栈序列。【中科院中国科技大学2005】

    A. 1、2、3、4                      B. 4、 3、2、1

    C. 1、3、4、2                      D.4、1、2、3

    选D

    我是一个个模拟来做的。

     

    描述栈的基本型性质:

    1、集合性:栈是由若干个元素集合而成,没有元素(空集)成为空栈。

    2、线性:除栈顶和栈底之外,任意元素均有唯一前趋和后继。

    3、运算受限:只在一端插入删除的线性表即为栈

     

    顺序存储和顺序存取:顺序存取是只能逐个存或取结构中的元素,例如栈。顺序存储是利用一个连续的空间相继存放,例如栈可基于一维数组存放元素。

     

    一个较早入栈的元素能否在后面元素之前出栈?如果后面元素压在它上面,就不可以了。如果后面元素未压入,它可以弹出。在其他元素前面。

     

     

    栈与递归:

      当在一个函数的运行期间调用另一个函数时,在运行 该被调用函数之前,需先完成三件事:  将实参等传递给被调用函数,保存返回地址(入栈);  为被调用函数的局部变量分配存储区;    将控制转移到被调用函数的入口。  

    从被调用函数返回调用函数之前,应该完成:  保存被调函数的计算结果;  释放被调函数的数据区;  按被调函数保存的返回地址(出栈)将控制转移到调        用函数。

    多个函数嵌套调用的规则是:后调用先返回。

     此时的内存管理实行“栈式管理”

     

    队列:

            在多用户计算机系统中,各个用户需要使用 CPU 运行自己的程序,它们分别向操作系统提出使用 CPU 的请求,操作系统按照每个请求在时间上的先后顺序, 将其排成一个队列,每次把CPU分配给队头用户使用, 当相应的程序运行结束,则令其出队,再把CPU分配 给新的队头用户,直到所有用户任务处理完毕。

     

    以主机和打印机为例来说明,主机输出数据给打印 机打印,主机输出数据的速度比打印机打印的速度要快 得多,若直接把输出的数据送给打印机打印,由于速度 不匹配,显然不行。解决的方法是设置一个打印数据缓 冲区,主机把要打印的数据依此写到这个缓冲区中,写 满后就暂停输出,继而去做其它的事情,打印机就从缓 冲区中按照先进先出的原则依次取出数据并打印,打印 完后再向主机发出请求,主机接到请求后再向缓冲区写 入打印数据,这样利用队列既保证了打印数据的正确, 又使主机提高了效率。

     

    双端队列:

    某队列允许在其两端进行入队操作,但仅允许在一端进行出队操作,若元素a,b,c,d,e依次入队列后,再进行出队操作,则不可能得到的顺序是( )。 

    A. bacde                B. dbace              C. dbcae                D. ecbad

    解析:出队只能一端,所以abcde一定是这个顺序。

    反模拟入队,每次只能在两边出元素。

     

    栈/队列 互相模拟实现

     

    用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

    思路:大概这么想:用一个辅助栈把进第一个栈的元素倒一下就好了。

    比如进栈1,2,3,4,5

    第一个栈:

    5

    4

    3

    2

    1

    然后倒到第二个栈里

    1

    2

    3

    4

    5

    再倒出来,顺序为1,2,3,4,5

    实现队列

    然后要注意的事情:

    1)栈2非空不能往里面倒数,顺序就错了。栈2没数再从栈1倒。

    2)栈1要倒就一次倒完,不倒完的话,进新数也会循序不对。

    import java.util.Stack;
     
    public class Solution {
        Stack<Integer> stack1 = new Stack<Integer>();
        Stack<Integer> stack2 = new Stack<Integer>();
         
        public void push(int node) {
            stack1.push(node);
        }
         
        public int pop() {
            if(stack1.empty()&&stack2.empty()){
                throw new RuntimeException("Queue is empty!");
            }
            if(stack2.empty()){
                while(!stack1.empty()){
                    stack2.push(stack1.pop());
                }
            }
            return stack2.pop();
        }
    }

     

    用两个队列实现栈,要求同上:

    这其实意义不是很大,有些数据结构书上甚至说两个队列不能实现栈。

    其实是可以的,只是时间复杂度较高,一个弹出操作时间为O(N)。

    思路:两个队列,编号为1和2.

    进栈操作:进1号队列

    出栈操作:把1号队列全弄到2号队列里,剩最后一个别压入,而是返回。

    最后还得把1和2号换一下,因为现在是2号有数,1号空。

     

    仅仅有思考价值,不实用。

    比如压入1,2,3

    队列1:1,2,3

    队列2:空

    依次弹出1,2,3:

    队列1里的23进入2号,3弹出

    队列1:空

    队列2:2,3

     

    队列2中3压入1号,2弹出

    队列1:3

    队列2:空

     

    队列1中只有一个元素,弹出。

     

    上代码:

    public class TwoQueueImplStack {
    	Queue<Integer> queue1 = new ArrayDeque<Integer>();
    	Queue<Integer> queue2 = new ArrayDeque<Integer>();
    //压入
    	public void push(Integer element){
    		//都为空,优先1
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    		//1为空,2有数据,放入2
    		if(queue1.isEmpty()){
    			queue2.add(element);
    			return;
    		}
    		//2为空,1有数据,放入1
    		if(queue2.isEmpty()){
    			queue1.add(element);
    			return;
    		}
    	}
    //弹出
    	public Integer pop(){
    		//两个都空,异常
    		if(queue1.isEmpty() && queue2.isEmpty()){
    			try{
    				throw new Exception("satck is empty!");
    			}catch(Exception e){
    				e.printStackTrace();
    			}
    		}	
    		//1空,2有数据,将2中的数据依次放入1,最后一个元素弹出
    		if(queue1.isEmpty()){
    			while(queue2.size() > 1){
    				queue1.add(queue2.poll());
    			}
    			return queue2.poll();
    		}
    		
    		//2空,1有数据,将1中的数据依次放入2,最后一个元素弹出
    		if(queue2.isEmpty()){
    			while(queue1.size() > 1){
    				queue2.add(queue1.poll());
    			}
    			return queue1.poll();
    		}
    		
    		return (Integer)null;
    	}
    //测试
    	public static void main(String[] args) {
    		TwoQueueImplStack qs = new TwoQueueImplStack();
    		qs.push(2);
    		qs.push(4);
    		qs.push(7);
    		qs.push(5);
    		System.out.println(qs.pop());
    		System.out.println(qs.pop());
    		
    		qs.push(1);
    		System.out.println(qs.pop());
    	}
    }
    

     

    第八次笔记 (串)

    串的概念:串(字符串):是由 0 个或多个字符组成的有限序列。 通常记为:s =‘ a1 a2 a3 … ai …an  ’ ( n≥0 )。

    串的逻辑结构和线性表极为相似。

     

    一些串的类型:

     

    空串:不含任何字符的串,长度 = 0。

    空格串:仅由一个或多个空格组成的串。

    子串:由串中任意个连续的字符组成的子序列。

    主串:包含子串的串。

    位置:字符在序列中的序号。

    子串在主串中的位置:子串的首字符在主串中的位置。

     

    空串是任意串的子串,任意串是其自身的子串。

    串相等的条件:当两个串的长度相等且各个对应位置的字符都相等时才相等。

     

    实现:

     

    因为串是特殊的线性表,故其存储结构与线性表的 存储结构类似,只不过组成串的结点是单个字符。

     

    定长顺序存储表示

    也称为静态存储分配的顺序串。 即用一组地址连续的存储单元依次存放串中的字符序列。

     

    串长:可能首字符记录(显式)或\0结尾(隐式)

     

    定长顺序存储表示时串操作的缺点 :串的某些操作受限(截尾),如串的联接、插入、置换

     

    堆分配存储表示  

     

    存储空间在程序执行过程中动态分配,malloc() 分配一块实际串长所需要的存储空间(“堆”)

    堆存储结构的优点:堆存储结构既有顺序存储 结构的特点,处理(随机取子串)方便,操作中对 串长又没有任何限制,更显灵活,因此在串处理的 应用程序中常被采用。

     

    串的块链存储表示

    为了提高空间利用率,可使每个结点存放多个字符 (这是顺序串和链串的综合 (折衷) ),称为块链结构。

     优点:便于插入和删除    缺点:空间利用率低 

     

    串的定长表示

    思想和代码都不难,和线性表也差不多,串本来就是数据受限的线性表。

    串连接:

     

    #include <stdio.h>
    #include <string.h>
    //串的定长顺序存储表示
    #define MAXSTRLEN 255							//用户可在255以内定义最大串长
    typedef unsigned char SString[MAXSTRLEN + 1];	//0号单元存放串的长度
    
    int Concat(SString *T,SString S1,SString S2)
    	//用T返回S1和S2联接而成的新串。若未截断返回1,若截断返回0
    {
    	int i = 1,j,uncut = 0;
    	if(S1[0] + S2[0] <= MAXSTRLEN)	//未截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    		for (j = 1; j <= S2[0]; j++)
    			(*T)[S1[0]+j] = S2[j];	//(*T)[i+j] = S2[j]
    		(*T)[0] = S1[0] + S2[0];
    		uncut = 1;
    	}
    	else if(S1[0] < MAXSTRLEN)		//截断
    	{
    		for (i = 1; i <= S1[0]; i++)//赋值时等号不可丢
    			(*T)[i] = S1[i];
    
    		for (j = S1[0] + 1; j <= MAXSTRLEN; j++)
    		{
    			(*T)[j] = S2[j - S1[0] ];
    			(*T)[0] = MAXSTRLEN;
    			uncut = 0;
    		}
    	}
    	else
    	{
    		for (i = 0; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		/*或者分开赋值,先赋值内容,再赋值长度
    		for (i = 1; i <= MAXSTRLEN; i++)
    			(*T)[i] = S1[i];
    		(*T)[0] = MAXSTRLEN;
    		*/
    		uncut = 0;
    	}
    
    	return uncut;
    }
    
    int SubString(SString *Sub,SString S,int pos,int len)
    	//用Sub返回串S的第pos个字符起长度为len的子串
    	//其中,1 ≤ pos ≤ StrLength(S)且0 ≤ len ≤ StrLength(S) - pos + 1(从pos开始到最后有多少字符)
    	//第1个字符的下标为1,因为第0个字符存放字符长度
    {
    	int i;
    	if(pos < 1 || pos > S[0] || len < 0 || len > S[0] - pos + 1)
    		return 0;
    	for (i = 1; i <= len; i++)
    	{
    		//S中的[pos,len]的元素 -> *Sub中的[1,len]
    		(*Sub)[i] = S[pos + i - 1];//下标运算符 > 寻址运算符的优先级
    	}
    	(*Sub)[0] = len;
    	return 1;
    }
    void PrintStr(SString S)
    {
    	int i;
    	for (i = 1; i <= S[0]; i++)
    		printf("%c",S[i]);
    	printf("\n");
    }
    
    
    int main(void)
    {
    	/*定长顺序存储初始化和打印的方法
    	SString s = {4,'a','b','c','d'};
    	int i;
    	//s = "abc";	//不可直接赋值
    	for (i = 1; i <= s[0]; i++)
    		printf("%c",s[i]);
    	*/
    	SString s1 = {4,'a','b','c','d'};
    	SString s2 = {4,'e','f','g','h'},s3;
    	SString T,Sub;
    	int i;
    	
    	for (i = 1; i <= 255; i++)
    	{
    		s3[i] = 'a';
    		if(i >= 248)
    			s3[i] = 'K';
    	}
    	s3[0] = 255;
    	SubString(&Sub,s3,247,8);
    	PrintStr(Sub);
    	
    
    
    
    	return 0;
    }

    第九次笔记(数组,广义表)

    数组:按一定格式排列起来的具有相同类型的数据元素的集合。

     

    二维数组:若一维数组中的数据元素又是一维数组结构,则称为二维数组。 

    同理,推广到多维数组。若 n -1 维数组中的元素又是一个一维数组结构,则称作 n 维数组。 

    声明格式:数据类型   变量名称[行数] [列数] ;

     

    实现:一般都是采用顺序存储结构来表示数组。

     

    二维数组两种顺序存储方式:以行序为主序 (低下标优先) 、以列序为主序 (高下标优先)

    一个二维数组 A,行下标的范围是 1 到 6,列下标的范围是 0 到 7,每个数组元素用相邻的6 个字节存储,存储器按字节编址。那么,这个数组的体积是288个字节。

     

     广义表(又称列表 Lists)是n≥0个元素 a1, a2, …, an 的有限序列,其中每一个ai 或者是原子,或者是一个子表。

     

    表头:若 LS 非空 (n≥1 ),则其第一个元素 a1 就是表头。

     表尾:除表头之外的其它元素组成的表。记作  tail(LS) = (a2, ..., an)。 

     

    (( )) 长度为 1,表头、表尾均为 ( )

    (a, (b, c))长度为 2,由原子 a 和子表 (b, c) 构成。表头为 a ;表尾为 ((b, c))。

     

    广义表的长度定义为最外层所包含元素的个数

    广义表的深度定义为该广义表展开后所含括号的重数。

    “原子”的深度为 0 ;  “空表”的深度为 1 。

     

    取表头运算 GetHead  和取表尾运算 GetTail

    GetHead(LS) = a1        GetTail(LS) = (a2, …, an)。

     

    广义表可看成是线性表的推广,线性表是广义表的特例。

     

    广义表的结构相当灵活,在某种前提下,它可以兼容线 性表、数组、树和有向图等各种常用的数据结构。

    由于广义表不仅集中了线性表、数组、树和有向图等常 见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。 

     

     

    第十次笔记(树和二叉树)

     

    树的定义:树(Tree)是 n(n≥0)个结点的有限集。若 n=0,称为空树;若 n > 0,则它满足如下两个条件:  

    (1)  有且仅有一个特定的称为根 (Root) 的结点;  

    (2)  其余结点可分为 m (m≥0) 个互不相交的有限集 T1, T2, T3, …, Tm, 其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。

    显然,树的定义是一个递归的定义。

    树的一些术语:

    • 结点的度(Degree):结点的子树个数;
    • 树的度:树的所有结点中最大的度数;
    • 叶结点(Leaf):度为0的结点;
    • 父结点(Parent):有子树的结点是其子树的根节点的父结点;
    • 子结点/孩子结点(Child):若A结点是B结点的父结点,则称B结点是A结点的子结点;
    • 兄弟结点(Sibling):具有同一个父结点的各结点彼此是兄弟结点;
    • 路径和路径长度:从结点n1到nk的路径为一个结点序列n1,n2,...,nk。ni是ni+1的父结点。路径所包含边的个数为路径的长度;
    • 祖先结点(Ancestor):沿树根到某一结点路径上的所有结点都是这个结点的祖先结点;
    • 子孙结点(Descendant):某一结点的子树中的所有结点是这个结点的子孙;
    • 结点的层次(Level):规定根结点在1层,其他任一结点的层数是其父结点的层数加1;
    • 树的深度(Depth):树中所有结点中的最大层次是这棵树的深度;

    将树中节点的各子树看成从左至右是有次序的(即不能互换),则称为该树是有序树,否则称为无序树

    森林(forest)是 m (m≥0) 棵互不相交的树的集合。

     

    二叉树

     

    在计算机科学中,二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

     

    虽然二叉树与树概念不同,但有关树的基本术语对二叉树都适用。

     

    二叉树结点的子树要区分左子树和右子树,即使只有一 棵子树也要进行区分,说明它是左子树,还是右子树。树当 结点只有一个孩子时,就无须区分它是左还是右。

     

    注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。

    一些性质:

    在二叉树的第 i 层上至多有  个结点 (i ≥1)。

    证明:每个节点至多两个孩子,每一层至多比上一层多一倍的结点,根为1.

     

    深度为 k 的二叉树至多有 个结点(k ≥1)。

    证明:把每一层最大节点加起来即可

     

    对任何一棵二叉树 T,如果其叶子数为 n0,度为 2的结点数为 n2,则 n0 = n2 + 1。

    证明:对于一个只有根的树,n0 = n2 + 1成立。1=0+1

    我们把一个叶子节点换成度为2的结点:

    黑色节点原来为叶子节点

    我们发现,度为2的结点数加1(黑色节点);叶子节点数加1(原来的去掉,新增两个);对于式子n0 = n2 + 1没影响,还是成立。

     

    我们把叶子节点换成度为1的结点,比如没有右孩子。

    我们发现,度为2的结点数没变。叶子节点数没变(减了一个加了一个)

    所以,不管你怎么换,公式都成立。(佛系证明)

     

     

    二叉树概述

     

    各种实现和应用以后放链接

    一、二叉树的基本概念

    二叉树:二叉树是每个节点最多有两个子树的树结构。

    根节点:一棵树最上面的节点称为根节点。

    父节点子节点:如果一个节点下面连接多个节点,那么该节点称为父节点,它下面的节点称为子 节点。

    叶子节点:没有任何子节点的节点称为叶子节点。

    兄弟节点:具有相同父节点的节点互称为兄弟节点。

    节点度:节点拥有的子树数。上图中,13的度为2,46的度为1,28的度为0。

    树的深度:从根节点开始(其深度为0)自顶向下逐层累加的。上图中,13的深度是1,30的深度是2,28的深度是3。

    树的高度:从叶子节点开始(其高度为0)自底向上逐层累加的。54的高度是2,根节点23的高度是3。

    对于树中相同深度的每个节点来说,它们的高度不一定相同,这取决于每个节点下面的叶子节点的深度。上图中,13和54的深度都是1,但是13的高度是1,54的高度是2。

    二、二叉树的类型

    类型定义图示

    满二叉树

    Full Binary Tree

    除最后一层无任何子节点外,每一层上的所有节点都有两个子节点,最后一层都是叶子节点。满足下列性质:

    1)一颗树深度为h,最大层数为k,深度与最大层数相同,k=h;

    2)叶子节点数(最后一层)为2k−1;

    3)第 i 层的节点数是:2i−1;

    4)总节点数是:2k−1,且总节点数一定是奇数。

    完全二叉树

    Complete Binary Tree

    若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。满足下列性质:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子节点只能在层次最大的两层上出现;

    2)对任一节点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个;

    3)除最后一层,第 i 层的节点数是:2i−1;

    4)有n个节点的完全二叉树,其深度为:log2n+1或为log2n+1;

    5)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    平衡二叉树

    Balanced Binary Tree

    又被称为AVL树,它是一颗空树或左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。

    二叉搜索树

    Binary Search Tree

    又称二叉查找树、二叉排序树(Binary Sort Tree)。它是一颗空树或是满足下列性质的二叉树:

    1)若左子树不空,则左子树上所有节点的值均小于或等于它的根节点的值;

    2)若右子树不空,则右子树上所有节点的值均大于或等于它的根节点的值;

    3)左、右子树也分别为二叉排序树。

    红黑树

    Red Black Tree

    是每个节点都带有颜色属性(颜色为红色或黑色)的自平衡二叉查找树,满足下列性质:

    1)节点是红色或黑色;

    2)根节点是黑色;

    3)所有叶子节点都是黑色;

    4)每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

    5)从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

     

     

     

     

     

     

     

     

     

    啦啦啦

     

     

    第十一次笔记(满二叉树,完全二叉树)

    因为图片丢失,内容不全,我尽量找一下图

    满二叉树 (Full binary tree)

    除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。

    国内教程定义:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。

    国外(国际)定义:a binary tree T is full if each node is either a leaf or possesses exactly two childnodes.

    大意为:如果一棵二叉树的结点要么是叶子结点,要么它有两个子结点,这样的树就是满二叉树。(一棵满二叉树的每一个结点要么是叶子结点,要么它有两个子结点,但是反过来不成立,因为完全二叉树也满足这个要求,但不是满二叉树)

    从图形形态上看,满二叉树外观上是一个三角形。

    这里缺失公式

    完全二叉树

     

    完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

    可以根据公式进行推导,假设n0是度为0的结点总数(即叶子结点数),n1是度为1的结点总数,n2是度为2的结点总数,则 :

    ①n= n0+n1+n2 (其中n为完全二叉树的结点总数);又因为一个度为2的结点会有2个子结点,一个度为1的结点会有1个子结点,除根结点外其他结点都有父结点,

    ②n= 1+n1+2*n2 ;由①、②两式把n2消去得:n= 2*n0+n1-1,由于完全二叉树中度为1的结点数只有两种可能0或1,由此得到n0=n/2 或 n0=(n+1)/2。

    简便来算,就是 n0=n/2,其中n为奇数时(n1=0)向上取整;n为偶数时(n1=1)。可根据完全二叉树的结点总数计算出叶子结点数。

     

    重点:出于简便起见,完全二叉树通常采用数组而不是链表存储

     

    对于tree[i]有如下特点:

    (1)若i为奇数且i>1,那么tree的左兄弟为tree[i-1];

    (2)若i为偶数且i<n,那么tree的右兄弟为tree[i+1];

    (3)若i>1,tree的父亲节点为tree[i div 2];

    (4)若2*i<=n,那么tree的左孩子为tree[2*i];若2*i+1<=n,那么tree的右孩子为tree[2*i+1];

    (5)若i>n div 2,那么tree[i]为叶子结点(对应于(3));

    (6)若i<(n-1) div 2.那么tree[i]必有两个孩子(对应于(4))。

    (7)满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

    完全二叉树第i层至多有2^(i-1)个节点,共i层的完全二叉树最多有2^i-1个节点。

    特点:

    1)只允许最后一层有空缺结点且空缺在右边,即叶子结点只能在层次最大的两层上出现;

    2)对任一结点,如果其右子树的深度为j,则其左子树的深度必为j或j+1。 即度为1的点只有1个或0个

     

    第十二次笔记(二叉树的存储和遍历)

     

    顺序存储结构

     

    完全二叉树:用一组地址连续的 存储单元依次自上而下、自左至右存 储结点元素,即将编号为 i  的结点元 素存储在一维数组中下标为 i –1 的分量中。

    一般二叉树:将其每个结点与完 全二叉树上的结点相对照,存储在一 维数组的相应分量中。

     

    最坏情况:树退化为线性后:

    我们要把它“变”成这个大家伙来存了:

    深度为 k 的且只 有 k 个结点的右单支树需要 长度为2^k-1 的一维数组。 

     

    链式存储结构

    lchild和rchild都是指向相同结构的指针

    在 n 个结点的二叉链表中有 n + 1 个空指针域。

    typedef struct BiTNode { // 结点结构
        TElemType data;
        struct BiTNode *lchild,*rchild;// 左右孩子指针
    } BiTNode, *BiTree;
    

    可以多一条指向父的指针。

     

    遍历二叉树

     

    顺着某一条搜索路径巡访二叉树中的结点,使   得每个结点均被访问一次,而且仅被访问一次

     “访问”的含义很广,可以是对结点作各种处理, 如:输出结点的信息、修改结点的数据值等,但要求这种访问不破坏原来的数据结构。

    (所以有些题目比如morris遍历、链表后半段反转判断回文等等必须进行完,解题时就算已经得出答案也要遍历完,因为我们不能改变原来的数据结构。)

     

    具体遍历的介绍

    https://blog.csdn.net/hebtu666/article/details/82853988

     

    深入理解二叉树遍历

    二叉树:二叉树是每个节点最多有两个子树的树结构。

     

    本文介绍二叉树的遍历相关知识。

    我们学过的基本遍历方法,无非那么几个:前序,中序,后序,还有按层遍历等等。

    设L、D、R分别表示遍历左子树、访问根结点和遍历右子树, 则对一棵二叉树的遍历有三种情况:DLR(称为先根次序遍历),LDR(称为中根次序遍历),LRD (称为后根次序遍历)。

    首先我们定义一颗二叉树

    typedef char ElementType;
    typedef struct TNode *Position;
    typedef Position BinTree;
    struct TNode{
        ElementType Data;
        BinTree Left;
        BinTree Right;
    };
    

    前序

    首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树

    思路:

    就是利用函数,先打印本个节点,然后对左右子树重复此过程即可。

    void PreorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        printf(" %c", BT->Data);
        PreorderTraversal(BT->Left);
        PreorderTraversal(BT->Right);
    }

     

    中序

    首先中序遍历左(右)子树,再访问根,最后中序遍历右(左)子树

    思路:

    还是利用函数,先对左边重复此过程,然后打印根,然后对右子树重复。

    void InorderTraversal( BinTree BT )
    {
        if(BT==NULL)return ;
        InorderTraversal(BT->Left);
        printf(" %c", BT->Data);
        InorderTraversal(BT->Right);
    }

    后序

    首先后序遍历左(右)子树,再后序遍历右(左)子树,最后访问根

    思路:

    先分别对左右子树重复此过程,然后打印根

    void PostorderTraversal(BinTree BT)
    {
        if(BT==NULL)return ;
        PostorderTraversal(BT->Left);
        PostorderTraversal(BT->Right);
        printf(" %c", BT->Data);
    }

    进一步思考

    看似好像很容易地写出了三种遍历。。。。。

     

    但是你真的理解为什么这么写吗?

    比如前序遍历,我们真的是按照定义里所讲的,首先访问根,再先序遍历左(右)子树,最后先序遍历右(左)子树。这种过程来遍历了一遍二叉树吗?

    仔细想想,其实有一丝不对劲的。。。

    再看代码:

    void Traversal(BinTree BT)//遍历
    {
    //1111111111111
        Traversal(BT->Left);
    //22222222222222
        Traversal(BT->Right);
    //33333333333333
    }

    为了叙述清楚,我给三个位置编了号 1,2,3

    我们凭什么能前序遍历,或者中序遍历,后序遍历?

    我们看,前序中序后序遍历,实现的代码其实是类似的,都是上面这种格式,只是我们分别在位置1,2,3打印出了当前节点而已啊。我们凭什么认为,在1打印,就是前序,在2打印,就是中序,在3打印,就是后序呢?不管在位置1,2,3哪里操作,做什么操作,我们利用函数遍历树的顺序变过吗?当然没有啊。。。

    都是三次返回到当前节点的过程:先到本个节点,也就是位置1,然后调用了其他函数,最后调用完了,我们开到了位置2。然后又调用别的函数,调用完了,我们来到了位置3.。然后,最后操作完了,这个函数才结束。代码里的三个位置,每个节点都被访问了三次。

    而且不管位置1,2,3打印了没有,操作了没有,这个顺序是永远存在的,不会因为你在位置1打印了,顺序就改为前序,你在位置2打印了,顺序就成了中序。

     

    为了有更直观的印象,我们做个试验:在位置1,2,3全都放入打印操作;

    我们会发现,每个节点都被打印了三次。而把每个数第一次出现拿出来,就组成了前序遍历的序列;所有数字第二次出现拿出来,就组成了中序遍历的序列。。。。

     

    其实,遍历是利用了一种数据结构:栈

    而我们这种写法,只是通过函数,来让系统帮我们压了栈而已。为什么能实现遍历?为什么我们访问完了左子树,能返回到当前节点?这都是栈的功劳啊。我们把当前节点(对于函数就是当时的现场信息)存到了栈里,记录下来,后来才能把它拿了出来,能回到以前的节点。

     

    想到这里,可能就有更深刻的理解了。

    我们能否不用函数,不用系统帮我们压栈,而是自己做一个栈,来实现遍历呢?

    先序实现思路:拿到一个节点的指针,先判断是否为空,不为空就先访问(打印)该结点,然后直接进栈,接着遍历左子树;为空则要从栈中弹出一个节点来,这个时候弹出的结点就是其父亲,然后访问其父亲的右子树,直到当前节点为空且栈为空时,结束。

    核心思路代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            //visit(p);进行操作
            st.push(p);//入栈
            p = p->lchild;左
        } 
        else//空
        {
            p = st.top();//取出
            st.pop();
            p = p->rchild;//右
        }
    }

    中序实现思路:和前序遍历一样,只不过在访问节点的时候顺序不一样,访问节点的时机是从栈中弹出元素时访问,如果从栈中弹出元素,就意味着当前节点父亲的左子树已经遍历完成,这时候访问父亲,就是中序遍历.

    (对应递归是第二次遇到)

    核心代码实现:

    *p=root;
    while(p || !st.empty())
    {
        if(p)//非空
        {
            st.push(p);//压入
            p = p->lchild;
        }
        else//空
        {
            p = st.top();//取出
            //visit(p);操作
            st.pop();
            p = p->rchild;
        }
    }

    后序遍历是最难的。因为要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难点。

    因为我们原来说了,后序是第三次遇到才进行操作的,所以我们很容易有这种和递归函数类似的思路:对于任一结点,将其入栈,然后沿其左子树一直往下走,一直走到没有左孩子的结点,此时该结点在栈顶,但是不能出栈访问, 因此右孩子还没访问。所以接下来按照相同的规则对其右子树进行相同的处理。访问完右孩子,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。

    第二种思路:对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,或者左孩子和右孩子都已被访问过了,就可以直接访问该结点。如果有孩子未访问,将P的右孩子和左孩子依次入栈。

    网上的思路大多是第一种,所以我在这里给出第二种的大概实现吧

    首先初始化cur,pre两个指针,代表访问的当前节点和之前访问的节点。把根放入,开始执行。

    s.push(root);
    while(!s.empty())
    {
        cur=s.top();
        if((cur->lchild==NULL && cur->rchild==NULL)||(pre!=NULL && (pre==cur->lchild||pre==cur->rchild)))
        {
            //visit(cur);  如果当前结点没有孩子结点或者孩子节点都已被访问过 
            s.pop();//弹出
            pre=cur; //记录
        }
        else//分别放入右左孩子
        {
            if(cur->rchild!=NULL)
                s.push(cur->rchild);
            if(cur->lchild!=NULL)    
                s.push(cur->lchild);
        }
    }

    这两种方法,都是利用栈结构来实现的遍历,需要一定的栈空间,而其实存在一种时间O(N),空间O(1)的遍历方式,下次写了我再放链接。

     

    斗个小机灵:后序是LRD,我们其实已经知道先序是DLR,那其实我们可以用先序来实现后序啊,我们只要先序的时候把左右子树换一下:DRL(这一步很好做到),然后倒过来不就是DRL了嘛。。。。。就把先序代码改的左右反过来,然后放栈里倒过来就好了,不需要上面介绍的那些复杂的方法。。。。

    第十四次笔记(树的存储)

     

     

    父节点表示法

     

    数据域:存放结点本身信息。

    双亲域:指示本结点的双亲结点在数组中的位置。

    对应的树:

    /* 树节点的定义 */
    #define MAX_TREE_SIZE 100
     
    typedef struct{
        TElemType data;
        int parent; /* 父节点位置域 */
    } PTNode;
     
    typedef struct{
        PTNode nodes[MAX_TREE_SIZE];
        int n; /* 节点数 */
    } PTree;

    特点:找双亲容易,找孩子难。

    孩子表示法(树的链式存储结构)

     

    childi指向一个结点

    可以加上parent。

    在有 n 个结点、度为  d 的树的 d 叉链表中,有  n×(d-1)+1 个空链域

     

    我们可以用degree记录有几个孩子,省掉空间,但是结点的指针个数不相等,为该结点的度 degree。

     

    孩子链表:

     

    把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,用顺序表(含 n 个元素的结构数组)存储。

    孩子兄弟表示法(二叉树表示法)

    用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点

    typedef struct CSNode{
         ElemType data;
         struct CSNode *firstchild, *nextsibling;  
    } CSNode, *CSTree;
    

    第十五次笔记(图基础)

     

    图是一种:   数据元素间存在多对多关系的数据结构   加上一组基本操作构成的抽象数据类型。

    图 (Graph) 是一种复杂的非线性数据结构,由顶点集合及顶点间的关系(也称弧或边)集合组成。可以表示为: G=(V, VR)  

    其中 V 是顶点的有穷非空集合;

    VR 是顶点之间   关系的有穷集合,也叫做弧或边集合。

    弧是顶点的有序对,边是顶点的无序对。

     

    特点:(相对于线性结构)

    顶点之间的关系是任意的 

    图中任意两个顶点之间都可能相关

    顶点的前驱和后继个数无限制

     

    相关概念:

     

    顶点(Vertex):图中的数据元素。线性表中我们把数据元素叫元素,树中将数据元素叫结点。

    边:顶点之间的逻辑关系用边来表示,边集可以是空的。

     

    无向边(Edge):若顶点V1到V2之间的边没有方向,则称这条边为无向边。

    无向图(Undirected graphs):图中任意两个顶点之间的边都是无向边。(A,D)=(D,A)

    无向图中边的取值范围:0≤e≤n(n-1)/2

    有向边:若从顶点V1到V2的边有方向,则称这条边为有向边,也称弧(Arc)。用<V1,V2>表示,V1为狐尾(Tail),V2为弧头(Head)。(V1,V2)≠(V2,V1)。

    有向图(Directed graphs):图中任意两个顶点之间的边都是有向边。

    有向图中弧的取值范围:0≤e≤n(n-1)

       注意:无向边用“()”,而有向边用“< >”表示。

     

    简单图:图中不存在顶点到其自身的边,且同一条边不重复出现。

    无向完全图:无向图中,任意两个顶点之间都存在边。

    有向完全图:有向图中,任意两个顶点之间都存在方向互为相反的两条弧。

    稀疏图:有很少条边。

    稠密图:有很多条边。

     

    邻接点:若 (v, v´) 是一条边,则称顶点 v 和 v´互为 邻接点,或称 v 和 v´相邻接;称边 (v, v´) 依附于顶点 v 和 v´,或称 (v, v´) 与顶点 v 和 v´ 相关联。

     

    权(Weight):与图的边或弧相关的数。

    网(Network):带权的图。

    子图(Subgraph):假设G=(V,{E})和G‘=(V',{E'}),如果V'包含于V且E'包含于E,则称G'为G的子图。

     

     入度:有向图中以顶点 v 为头的弧的数目称为 v 的入度,记为:ID(v)。  

    出度:有向图中以顶点 v 为尾的弧的数目称为 v 的出度,记为:OD(v)。

    度(Degree):无向图中,与顶点V相关联的边的数目。有向图中,入度表示指向自己的边的数目,出度表示指向其他边的数目,该顶点的度等于入度与出度的和。

     

    回路(环):第一个顶点和最后一个顶点相同的路径。

    简单路径:序列中顶点(两端点除外)不重复出现的路径。 

    简单回路(简单环):前后两端点相同的简单路径。

    路径的长度:一条路径上边或弧的数量。

     

    连通:从顶点 v 到 v´ 有路径,则说 v  和 v´ 是连通的。

    连通图:图中任意两个顶点都是连通的。

    连通分量:无向图的极大连通子图(不存在包含它的 更大的连通子图);

    任何连通图的连通分量只有一个,即其本身;非连通图有多个连通分量(非连通图的每一个连通部分)。

    强连通图: 任意两个顶点都连通的有向图。 

    强连通分量:有向图的极大强连通子图;任何强连通 图的强连通分量只有一个,即其本身;非强连通图有多个 强连通分量。

     

    生成树:所有顶点均由边连接在一起但不存在回路的图。(n个顶点n-1条边)

     

     

     

     

    图的存储

     

    多重链表:完全模拟图的样子,每个节点内的指针都指向该指向的节点。

    节点结构内指针数为度

    缺点:浪费空间、不容易操作

     

    数组表示法(邻接矩阵表示法)

    可用两个数组存储。其中一个 一维数组存储数据元素(顶点)的信息,另一个二维数组 (邻接矩阵)存储数据元素之间的关系(边或弧)的信息

    有向图:

    有向网:

    缺点:用于稀疏图时空间浪费严重

    优点:操作较容易

     

    邻接表

    指针数组存放每个结点,每个结点后接所有能到达的节点。

     

    图的遍历

     

    从图的任意指定顶点出发,依照某种规则去访问图中所有顶 点,且每个顶点仅被访问一次,这一过程叫做图的遍历。

    图的遍历按照深度优先和广度优先规则去实施,通常有深度 优先遍历法(Depth_First Search——DFS )和  广度优先遍历法 ( Breadth_Frist Search——BFS)两种。

    简单棋盘搜索https://blog.csdn.net/hebtu666/article/details/81483407

    别的实现以后再贴

    如何判别V的邻接点是否被访问?

    为每个顶点设立一个“访问标志”。

     

    最小生成树

    问题提出:
        要在n个城市间建立通信联络网。顶点:表示城市,权:城市间通信线路的花费代价。希望此通信网花费代价最小。
    问题分析:
        答案只能从生成树中找,因为要做到任何两个城市之间有线路可达,通信网必须是连通的;但对长度最小的要求可以知道网中显然不能有圈,如果有圈,去掉一条边后,并不破坏连通性,但总代价显然减少了,这与总代价最小的假设是矛盾的。
    结论:
        希望找到一棵生成树,它的每条边上的权值之和(即建立该通信网所需花费的总代价)最小 —— 最小代价生成树。
        构造最小生成树的算法很多,其中多数算法都利用了一种称之为 MST 的性质。
        MST 性质:设 N = (V, E)  是一个连通网,U是顶点集 V的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中u∈U,v∈V-U,则必存在一棵包含边 (u, v) 的最小生成树。


    (1)普里姆 (Prim) 算法

    算法思想: 
        ①设 N=(V, E)是连通网,TE是N上最小生成树中边的集合。
        ②初始令 U={u_0}, (u_0∈V), TE={ }。
        ③在所有u∈U,u∈U-V的边(u,v)∈E中,找一条代价最小的边(u_0,v_0 )。
        ④将(u_0,v_0 )并入集合TE,同时v_0并入U。
        ⑤重复上述操作直至U = V为止,则 T=(V,TE)为N的最小生成树。

     
    代码实现:

    void MiniSpanTree_PRIM(MGraph G,VertexType u)
        //用普里姆算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边。
        //记录从顶点集U到V-U的代价最小的边的辅助数组定义;
        //closedge[j].lowcost表示在集合U中顶点与第j个顶点对应最小权值
    {
        int k, j, i;
        k = LocateVex(G,u);
        for (j = 0; j < G.vexnum; ++j)    //辅助数组的初始化
            if(j != k)
            {
                closedge[j].adjvex = u;
                closedge[j].lowcost = G.arcs[k][j].adj;    
    //获取邻接矩阵第k行所有元素赋给closedge[j!= k].lowcost
            }
        closedge[k].lowcost = 0;        
    //初始,U = {u};  
        PrintClosedge(closedge,G.vexnum);
        for (i = 1; i < G.vexnum; ++i)    \
    //选择其余G.vexnum-1个顶点,因此i从1开始循环
        {
            k = minimum(G.vexnum,closedge);        
    //求出最小生成树的下一个结点:第k顶点
            PrintMiniTree_PRIM(G, closedge, k);     //输出生成树的边
            closedge[k].lowcost = 0;                //第k顶点并入U集
            PrintClosedge(closedge,G.vexnum);
            for(j = 0;j < G.vexnum; ++j)
            {                                           
                if(G.arcs[k][j].adj < closedge[j].lowcost)    
    //比较第k个顶点和第j个顶点权值是否小于closedge[j].lowcost
                {
                    closedge[j].adjvex = G.vexs[k];//替换closedge[j]
                    closedge[j].lowcost = G.arcs[k][j].adj;
                    PrintClosedge(closedge,G.vexnum);
                }
            }
        }
    }


    (2)克鲁斯卡尔 (Kruskal) 算法

    算法思想: 
        ①设连通网  N = (V, E ),令最小生成树初始状态为只有n个顶点而无边的非连通图,T=(V, { }),每个顶点自成一个连通分量。
        ②在 E 中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
    ③依此类推,直至 T 中所有顶点都在同一连通分量上为止。
          
        最小生成树可能不惟一!

     

    拓扑排序

    (1)有向无环图

        无环的有向图,简称 DAG (Directed Acycline Graph) 图。
     
    有向无环图在工程计划和管理方面的应用:除最简单的情况之外,几乎所有的工程都可分为若干个称作“活动”的子工程,并且这些子工程之间通常受着一定条件的约束,例如:其中某些子工程必须在另一些子工程完成之后才能开始。
    对整个工程和系统,人们关心的是两方面的问题: 
    ①工程能否顺利进行; 
    ②完成整个工程所必须的最短时间。

    对应到有向图即为进行拓扑排序和求关键路径。 
    AOV网: 
        用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
    例如:排课表
          
    AOV网的特点:
    ①若从i到j有一条有向路径,则i是j的前驱;j是i的后继。
    ②若< i , j >是网中有向边,则i是j的直接前驱;j是i的直接后继。
    ③AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。


    问题:    
        问题:如何判别 AOV 网中是否存在回路?
        检测 AOV 网中是否存在环方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。


    拓扑排序的方法:
        ①在有向图中选一个没有前驱的顶点且输出之。
        ②从图中删除该顶点和所有以它为尾的弧。
        ③重复上述两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
            
        一个AOV网的拓扑序列不是唯一的!
    代码实现:

    Status TopologicalSort(ALGraph G)
        //有向图G采用邻接表存储结构。
        //若G无回路,则输出G的顶点的一个拓扑序列并返回OK,否则返回ERROR.
        //输出次序按照栈的后进先出原则,删除顶点,输出遍历
    {
        SqStack S;
        int i, count;
        int *indegree1 = (int *)malloc(sizeof(int) * G.vexnum);
        int indegree[12] = {0};
        FindInDegree(G, indegree);    //求个顶点的入度下标从0开始
        InitStack(&S);
        PrintStack(S);
        for(i = 0; i < G.vexnum; ++i)
            if(!indegree[i])        //建0入度顶点栈S
                push(&S,i);        //入度为0者进栈
        count = 0;                //对输出顶点计数
        while (S.base != S.top)
        {
            ArcNode* p;
            pop(&S,&i);
            VisitFunc(G,i);//第i个输出栈顶元素对应的顶点,也就是最后进来的顶点    
            ++count;          //输出i号顶点并计数
            for(p = G.vertices[i].firstarc; p; p = p->nextarc)
            {    //通过循环遍历第i个顶点的表结点,将表结点中入度都减1
                int k = p->adjvex;    //对i号顶点的每个邻接点的入度减1
                if(!(--indegree[k]))
                    push(&S,k);        //若入度减为0,则入栈
            }//for
        }//while
        if(count < G.vexnum)
        {
            printf("\n该有向图有回路!\n");
            return ERROR;    //该有向图有回路
        }
        else
        {
            printf("\n该有向图没有回路!\n");
            return OK;
        }
    }


    关键路径

        把工程计划表示为有向图,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。称这种有向图为边表示活动的网,简称为 AOE网 (Activity On Edge)。
    例如:
    设一个工程有11项活动,9个事件。
    事件v_1——表示整个工程开始(源点) 
    事件v_9——表示整个工程结束(汇点)

     
    对AOE网,我们关心两个问题:  
    ①完成整项工程至少需要多少时间? 
    ②哪些活动是影响工程进度的关键?
    关键路径——路径长度最长的路径。
    路径长度——路径上各活动持续时间之和。
    v_i——表示事件v_i的最早发生时间。假设开始点是v_1,从v_1到〖v�i〗的最长路径长度。ⅇ(ⅈ)——表示活动a_i的最早发生时间。
    l(ⅈ)——表示活动a_i最迟发生时间。在不推迟整个工程完成的前提下,活动a_i最迟必须开始进行的时间。
    l(ⅈ)-ⅇ(ⅈ)意味着完成活动a_i的时间余量。
    我们把l(ⅈ)=ⅇ(ⅈ)的活动叫做关键活动。显然,关键路径上的所有活动都是关键活动,因此提前完成非关键活动并不能加快工程进度。
        例如上图中网,从从v_1到v_9的最长路径是(v_1,v_2,v_5,v_8,ν_9 ),路径长度是18,即ν_9的最迟发生时间是18。而活动a_6的最早开始时间是5,最迟开始时间是8,这意味着:如果a_6推迟3天或者延迟3天完成,都不会影响整个工程的完成。因此,分析关键路径的目的是辨别哪些是关键活动,以便争取提高关键活动的工效,缩短整个工期。
        由上面介绍可知:辨别关键活动是要找l(ⅈ)=ⅇ(ⅈ)的活动。为了求ⅇ(ⅈ)和l(ⅈ),首先应求得事件的最早发生时间vⅇ(j)和最迟发生时间vl(j)。如果活动a_i由弧〈j,k〉表示,其持续时间记为dut(〈j,k〉),则有如下关系:
    ⅇ(ⅈ)= vⅇ(j)
    l(ⅈ)=vl(k)-dut(〈j,k〉)
        求vⅇ(j)和vl(j)需分两步进行:
    第一步:从vⅇ(0)=0开始向前递推
    vⅇ(j)=Max{vⅇ(i)+dut(〈j,k〉)}   〈i,j〉∈T,j=1,2,…,n-1
    其中,T是所有以第j个顶点为头的弧的集合。
    第二步:从vl(n-1)=vⅇ(n-1)起向后递推
    vl(i)=Min{vl(j)-dut(〈i,j〉)}  〈i,j〉∈S,i=n-2,…,0
    其中,S是所有以第i个顶点为尾的弧的集合。
    下面我们以上图AOE网为例,先求每个事件v_i的最早发生时间,再逆向求每个事件对应的最晚发生时间。再求每个活动的最早发生时间和最晚发生时间,如下面表格:
              
    在活动的统计表中,活动的最早发生时间和最晚发生时间相等的,就是关键活动


    关键路径的讨论:

    ①若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。      如:a11、a10、a8、a7。 
    ②如果一个活动处于所有的关键路径上,则提高这个活动的速度,就能缩短整个工程的完成时间。如:a1、a4。
    ③处于所有关键路径上的活动完成时间不能缩短太多,否则会使原关键路径变成非关键路径。这时必须重新寻找关键路径。如:a1由6天变成3天,就会改变关键路径。

    关键路径算法实现:

    int CriticalPath(ALGraph G)
    {    //因为G是有向网,输出G的各项关键活动
        SqStack T;
        int i, j;    ArcNode* p;
        int k , dut;
        if(!TopologicalOrder(G,T))
            return 0;
        int vl[VexNum];
        for (i = 0; i < VexNum; i++)
            vl[i] = ve[VexNum - 1];        //初始化顶点事件的最迟发生时间
        while (T.base != T.top)            //按拓扑逆序求各顶点的vl值
        {
     
            for(pop(&T, &j), p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                k = p->adjvex;    dut = *(p->info);    //dut<j, k>
                if(vl[k] - dut < vl[j])
                    vl[j] = vl[k] - dut;
            }//for
        }//while
        for(j = 0; j < G.vexnum; ++j)    //求ee,el和关键活动
        {
            for (p = G.vertices[j].firstarc; p; p = p->nextarc)
            {
                int ee, el;        char tag;
                k = p->adjvex;    dut = *(p->info);
                ee = ve[j];    el = vl[k] - dut;
                tag = (ee == el) ? '*' : ' ';
                PrintCriticalActivity(G,j,k,dut,ee,el,tag);
            }
        }
        return 1;
    }

     

    最短路 

    最短路

        典型用途:交通网络的问题——从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条路最短?
     
        交通网络用有向网来表示:顶点——表示城市,弧——表示两个城市有路连通,弧上的权值——表示两城市之间的距离、交通费或途中所花费的时间等。
        如何能够使一个城市到另一个城市的运输时间最短或运费最省?这就是一个求两座城市间的最短路径问题。
        问题抽象:在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n - 1条边。
       常见最短路径问题:单源点最短路径、所有顶点间的最短路径
    (1)如何求得单源点最短路径?
        穷举法:将源点到终点的所有路径都列出来,然后在其中选最短的一条。但是,当路径特别多时,特别麻烦;没有规律可循。
        迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生各顶点的最短路径。
    路径长度最短的最短路径的特点:
        在此路径上,必定只含一条弧 <v_0, v_1>,且其权值最小。由此,只要在所有从源点出发的弧中查找权值最小者。
    下一条路径长度次短的最短路径的特点:
    ①、直接从源点到v_2<v_0, v_2>(只含一条弧);
    ②、从源点经过顶点v_1,再到达v_2<v_0, v_1>,<v_1, v_2>(由两条弧组成)
    再下一条路径长度次短的最短路径的特点:
        有以下四种情况:
        ①、直接从源点到v_3<v_0, v_3>(由一条弧组成);
        ②、从源点经过顶点v_1,再到达v_3<v_0, v_1>,<v_1, v_3>(由两条弧组成);
        ③、从源点经过顶点v_2,再到达v_3<v_0, v_2>,<v_2, v_3>(由两条弧组成);
        ④、从源点经过顶点v_1  ,v_2,再到达v_3<v_0, v_1>,<v_1, v_2>,<v_2, v_3>(由三条弧组成);
    其余最短路径的特点:    
        ①、直接从源点到v_i<v_0, v_i>(只含一条弧);
        ②、从源点经过已求得的最短路径上的顶点,再到达v_i(含有多条弧)。
    Dijkstra算法步骤:
        初始时令S={v_0},  T={其余顶点}。T中顶点对应的距离值用辅助数组D存放。
        D[i]初值:若<v_0, v_i>存在,则为其权值;否则为∞。 
        从T中选取一个其距离值最小的顶点v_j,加入S。对T中顶点的距离值进行修改:若加进v_j作中间顶点,从v_0到v_i的距离值比不加 vj 的路径要短,则修改此距离值。
        重复上述步骤,直到 S = V 为止。

    算法实现:

    void ShortestPath_DIJ(MGraph G,int v0,PathMatrix &P,ShortPathTable &D)
    { // 用Dijkstra算法求有向网 G 的 v0 顶点到其余顶点v的最短路径P[v]及带权长度D[v]。
        // 若P[v][w]为TRUE,则 w 是从 v0 到 v 当前求得最短路径上的顶点。  P是存放最短路径的矩阵,经过顶点变成TRUE
        // final[v]为TRUE当且仅当 v∈S,即已经求得从v0到v的最短路径。
        int v,w,i,j,min;
        Status final[MAX_VERTEX_NUM];
        for(v = 0 ;v < G.vexnum ;++v)
        {
            final[v] = FALSE;
            D[v] = G.arcs[v0][v].adj;        //将顶点数组中下标对应是 v0 和 v的距离给了D[v]
            for(w = 0;w < G.vexnum; ++w)
                P[v][w] = FALSE;            //设空路径
            if(D[v] < INFINITY)
            {
                P[v][v0] = TRUE;
                P[v][v] = TRUE;
            }
        }
        D[v0]=0;
        final[v0]= TRUE; /* 初始化,v0顶点属于S集 */
        for(i = 1;i < G.vexnum; ++i) /* 其余G.vexnum-1个顶点 */
        { /* 开始主循环,每次求得v0到某个v顶点的最短路径,并加v到S集 */
            min = INFINITY; /* 当前所知离v0顶点的最近距离 */
            for(w = 0;w < G.vexnum; ++w)
                if(!final[w]) /* w顶点在V-S中 */
                    if(D[w] < min)
                    {
                        v = w;
                        min = D[w];
                    } /* w顶点离v0顶点更近 */
                    final[v] = TRUE; /* 离v0顶点最近的v加入S集 */
                    for(w = 0;w < G.vexnum; ++w) /* 更新当前最短路径及距离 */
                    {
                        if(!final[w] && min < INFINITY && G.arcs[v][w].adj < INFINITY && (min + G.arcs[v][w].adj < D[w]))
                        { /* 修改D[w]和P[w],w∈V-S */
                            D[w] = min + G.arcs[v][w].adj;
                            for(j = 0;j < G.vexnum;++j)
                                P[w][j] = P[v][j];
                            P[w][w] = TRUE;
                        }
                    }
        }
    }

     

    经典二分问题

    经典二分问题:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  。

    写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
    示例 1:

    输入: nums = [-1,0,3,5,9,12], target = 9。输出: 4
    解释: 9 出现在 nums 中并且下标为 4
    示例 2:

    输入: nums = [-1,0,3,5,9,12], target = 2。输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    思路1:我们当然可以一个数一个数的遍历,但是毫无疑问要被大妈鄙视,这可怎么办呢?

    思路2:二分查找
    二分查找是一种基于比较目标值和数组中间元素的教科书式算法。

    如果目标值等于中间元素,则找到目标值。
    如果目标值较小,证明目标值小于中间元素及右边的元素,继续在左侧搜索。
    如果目标值较大,证明目标值大于中间元素及左边的元素,继续在右侧搜索。

    算法代码描述:

    初始化指针 left = 0, right = n - 1。
    当 left <= right:
    比较中间元素 nums[pivot] 和目标值 target 。
    如果 target = nums[pivot],返回 pivot。
    如果 target < nums[pivot],则在左侧继续搜索 right = pivot - 1。
    如果 target > nums[pivot],则在右侧继续搜索 left = pivot + 1。

    算法实现:照例贴出三种语言的实现,在Java实现中给出了详细注释

    class Solution {
      public int search(int[] nums, int target) {
    	//分别准备好左右端点
        int left = 0, right = nums.length - 1;
    	//循环二分
        while (left <= right) {
    	//取中点
          int pivot = left + (right - left) / 2;
    	  //找到答案并返回
          if (nums[pivot] == target) return pivot;
    	  //向左继续找
          if (target < nums[pivot]) right = pivot - 1;
    	  //向右继续找
          else left = pivot + 1;
        }
    	//未找到,返回-1
        return -1;
      }
    }
    class Solution:
        def search(self, nums: List[int], target: int) -> int:
            left, right = 0, len(nums) - 1
            while left <= right:
                pivot = left + (right - left) // 2
                if nums[pivot] == target:
                    return pivot
                if target < nums[pivot]:
                    right = pivot - 1
                else:
                    left = pivot + 1
            return -1
    class Solution {
      public:
      int search(vector<int>& nums, int target) {
        int pivot, left = 0, right = nums.size() - 1;
        while (left <= right) {
          pivot = left + (right - left) / 2;
          if (nums[pivot] == target) return pivot;
          if (target < nums[pivot]) right = pivot - 1;
          else left = pivot + 1;
        }
        return -1;
      }
    };

     

     

     

    二叉搜索树实现

    本文给出二叉搜索树介绍和实现

     

    首先说它的性质:所有的节点都满足,左子树上所有的节点都比自己小,右边的都比自己大。

     

    那这个结构有什么有用呢?

    首先可以快速二分查找。还可以中序遍历得到升序序列,等等。。。

    基本操作:

    1、插入某个数值

    2、查询是否包含某个数值

    3、删除某个数值

     

    根据实现不同,还可以实现其他很多种操作。

     

    实现思路思路:

    前两个操作很好想,就是不断比较,大了往左走,小了往右走。到空了插入,或者到空都没找到。

    而删除稍微复杂一些,有下面这几种情况:

    1、需要删除的节点没有左儿子,那就把右儿子提上去就好了。

    2、需要删除的节点有左儿子,这个左儿子没有右儿子,那么就把左儿子提上去

    3、以上都不满足,就把左儿子子孙中最大节点提上来。

     

    当然,反过来也是成立的,比如右儿子子孙中最小的节点。

     

    下面来叙述为什么可以这么做。

    下图中A为待删除节点。

    第一种情况:

     

    1、去掉A,把c提上来,c也是小于x的没问题。

    2、根据定义可知,x左边的所有点都小于它,把c提上来不影响规则。

     

    第二种情况

     

    3、B<A<C,所以B<C,根据刚才的叙述,B可以提上去,c可以放在b右边,不影响规则

    4、同理

     

    第三种情况

     

    5、注意:是把黑色的提升上来,不是所谓的最右边的那个,因为当初向左拐了,他一定小。

    因为黑色是最大,比B以及B所有的孩子都大,所以让B当左孩子没问题

    而黑点小于A,也就小于c,所以可以让c当右孩子

    大概证明就这样。。

    下面我们用代码实现并通过注释理解

    上次链表之类的用的c,循环来写的。这次就c++函数递归吧,不同方式练习。

    定义

    struct node
    {
        int val;//数据
        node *lch,*rch;//左右孩子
    };

    插入

     node *insert(node *p,int x)
     {
         if(p==NULL)//直到空就创建节点
         {
             node *q=new node;
             q->val=x;
             q->lch=q->rch=NULL;
             return p;
         }
         if(x<p->val)p->lch=insert(p->lch,x);
         else p->lch=insert(p->rch,x);
         return p;//依次返回自己,让上一个函数执行。
     }

    查找

     bool find(node *p,int x)
     {
         if(p==NULL)return false;
         else if(x==p->val)return true;
         else if(x<p->val)return find(p->lch,x);
         else return find(p->rch,x);
     }

    删除

     node *remove(node *p,int x)
     {
          if(p==NULL)return NULL;
          else if(x<p->val)p->lch=remove(p->lch,x);
          else if(x>p->val)p->lch=remove(p->rch,x);
          //以下为找到了之后
          else if(p->lch==NULL)//情况1
          {
              node *q=p->rch;
              delete p;
              return q;
          }
          else if(p->lch->rch)//情况2
          {
              node *q=p->lch;
              q->rch=p->rch;
              delete p;
              return q;
          }
          else
          {
              node *q;
              for(q=p->lch;q->rch->rch!=NULL;q=q->rch);//找到最大节点的前一个
              node *r=q->rch;//最大节点
              q->rch=r->lch;//最大节点左孩子提到最大节点位置
              r->lch=p->lch;//调整黑点左孩子为B
              r->rch=p->rch;//调整黑点右孩子为c
              delete p;//删除
              return r;//返回给父
          }
          return p;
     }

     

    对数组排序可以说是编程基础中的基础,本文对八种排序方法做简要介绍并用python实现。

    代码中注释很全,适合复习和萌新学习。这是刚入学自己写的,可能难免比不上标准的写法,但是懒得改了。

    文末会放和排序相关的基本拓展总结链接。

    看不明白可以看群里视频

    注意排序实现的具体方式,不要用局部变量,否则占空间太多,和空间复杂度不符。

    好,我们开始。

    • 选择排序

    选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在待排序序列的起始位置,直到全部待排序的数据元素排完。时间复杂度O(N^2)

    for i in range(len(l)):#意义是第i个位置开始挑第i大(小)的元素
        for j in range(i,len(l)):#和其他待排序的元素比较
    	if l[j]<l[i]:#更大就交换
    	    l[j],l[i]=l[i],l[j]
    • 冒泡排序

    冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法

    它重复地走访过要排序的元素列,一次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换(一般进行n次即可,第n次一定会把第n小的元素放到正确位置)。

    这个算法的名字由来是因为越大的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。时间复杂度O(N^2)

    for i in range(len(l)-1):#下标和i无关,代表的只是第i次排序,最多需要len(l)-1次排序即可
        for j in range(len(l)-1):#遍历每一个元素
    	if l[j]<l[j+1]:#本元素比下一个元素小,就交换
    		l[j],l[j+1]=l[j+1],l[j]

     分析一下其实每次排序都会多一个元素已经确定了位置,不需要再次遍历。

    所以j循环可以改成len(l)-i-1

    时间复杂度没变。

     

    • 插入排序

    有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,算法适用于少量数据的排序,时间复杂度为O(n^2)。是稳定的排序方法。

    for i in range(1,len(l)):#意义是第i个元素开始插入i之前的序列(已经有序)
        for j in range(i,0,-1):#只要比它之前的元素小就交换
    	if l[j]<l[j-1]:
    	    l[j],l[j-1]=l[j-1],l[j]
    	else:
                break#直到比前一个元素大

     

    • 归并排序

    速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列

    试想:假设已经有两个有序数列,分别存放在两个数组s,r中,我们如何把他们有序的合并在一起?

    归并排序就是在重复这样的过程,首先单个元素合并为含有两个元素的数组(有序),然后这种数组再和同类数组合并为四元素数组,以此类推,直到整个数组合并完毕。

    def gg(l,ll):#合并函数
        a,b=0,0
        k=[]#用来合并的列表
        while a<len(l) and b<len(ll):#两边都非空
            if l[a]<ll[b]:
                k.append(l[a])
                a=a+1
            elif l[a]==ll[b]:a=a+1#实现去重
            else:
                k.append(ll[b])
                b=b+1
        k=k+l[a:]+ll[b:]#加上剩下的
        return k
    
    def kk(p):#分到只剩一个元素就开始合并
        if len(p)<=1:
            return p
        a=kk(p[0:len(p)//2])#不止一个元素就切片
        b=kk(p[len(p)//2:])
        return gg(a,b)#返回排好序的一部分
    l=list(map(int,input().split(" ")))
    print(kk(l))
    • 快速排序

    快速排序(Quicksort)是对冒泡排序的一种改进。

    快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列

    • 随机化快排

    快速排序的最坏情况基于每次划分对主元的选择。基本的快速排序选取第一个元素作为主元。这样在数组已经有序的情况下,每次划分将得到最坏的结果。比如1 2 3 4 5,每次取第一个元素,就退化为了O(N^2)。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为主元。

    这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度

    进一步提升可以分割为三部分,即小于区,等于区,大于区,减小了递归规模,并克服了多元素相同的退化。

    def gg(a,b):
        global l
        if a>=b:#注意停止条件,我以前没加>卡了半小时
            return
        x,y=a,b
        import random#为了避免遇到基本有序序列退化,随机选点
        g=random.randint(a,b)
        l[g],l[y]=l[y],l[g]#交换选中元素和末尾元素
        while a<b:
            if l[a]>l[y]:#比目标元素大
                l[a],l[b-1]=l[b-1],l[a]#交换
                b=b-1#大于区扩大
                #注意:换过以后a不要加,因为新换过来的元素并没有判断过
            else:a=a+1#小于区扩大
        l[y],l[a]=l[a],l[y]#这时a=b
        #现在解释a和b:a的意义是小于区下一个元素
        #b是大于区的第一个元素
        gg(x,a-1)#左右分别递归
        gg(a+1,y)
    
    l=list(map(int,input().split(" ")))
    gg(0,len(l)-1)
    print(l)
    
    • 堆排序

    堆排序(HeapSort)是一树形选择排序。堆排序的特点是:在排序过程中,将R[l..n]看成是一棵完全二叉树顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。

    由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。

    堆排序是就地排序,辅助空间为O(1).

    它是不稳定的排序方法。

    主要思想:维持一个大根堆(根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆。注意:①堆中任一子树亦是堆。②以上讨论的堆实际上是二叉堆(Binary Heap),类似地可定义k叉堆。)

    第一步:通过调整原地建立大根堆

    第二步:每次交换堆顶和边界元素,并减枝,然后调整堆顶下沉到正确位置。

    def down(i,k):#在表l里的第i元素调整,k为边界
    
    #优先队列也是通过这种方式实现的
        global l
        while 2*i+2<k:#右孩子不越界
            lift,right=2*i+1,2*i+2
            m=max(l[i],l[lift],l[right])
            if m==l[i]:#不需要调
                break
            if m==l[lift]:#把最大的换上来
                l[i],l[lift]=l[lift],l[i]
                i=lift#目的节点下标更新
            else:#把最大的换上来
                l[i],l[right]=l[right],l[i]
                i=right#目的节点下标更新
        if 2*i+1<k:#判断左孩子
            if l[2*i+1]>l[i]:
                l[i],l[2*i+1]=l[2*i+1],l[i]
    
    def main():
        global l
        for j in range(1,len(l)+1):#调大根堆
            i=len(l)-j
            down(i,len(l))
        for i in range(len(l)-1,-1,-1):#排序
            l[i],l[0]=l[0],l[i]#最大和边界交换,剪枝
            down(0,i)
        print(l)
        
    l=list(map(int,input().split(" ")))
    main()
    
            
        
    
            
    
    • 桶排序

    桶排序不是基于比较的排序方法,只需对号入座。将相应的数字放进相应编号的桶即可。

    当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间o(n)

    对于海量有范围数据十分适合,比如全国高考成绩排序,公司年龄排序等等。

    l=list(map(int,input().split(" ")))
    n=max(l)-min(l)
    p=[0]*(n+1)#为了省空间
    for i in l:
        p[i-min(l)]=1#去重排序,做标记即可
    for i in range(n):
        if p[i]==1:#判断是否出现过
            print(i+min(l),end=" ")
    • 希尔排序

    希尔排序(Shell's Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因D.L.Shell于1959年提出而得名。

    通过缩小有序步长来实现。

     

    def shell(arr):
     n=len(arr)#初始化步长
     h=1
     while h<n/3:
      h=3*h+1
     while h>=1:#判断,退出后就有序了。
      for i in range(h,n):
       j=i
       while j>=h and arr[j]<arr[j-h]:#判断是否交换
        arr[j], arr[j-h] = arr[j-h], arr[j]
        j-=h
      h=h//3#逐渐缩小步长
     print arr

    稳定性及时间复杂度

    排序稳定性概念:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。

    时间复杂度:时间复杂度是同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率。算法分析的目的在于选择合适算法和改进算法。可以理解为和常数操作所成的一种关系(常数操作为O(1))

    空间复杂度类似。

    下面给出各类排序的对比图:

     

     

     

    • 基数排序

    因为桶排序是稳定的,基数排序就是很多次桶排序而已,按位进行桶排序即可。

    (个人认为桶排序名字不恰当,因为桶是先进后出,和稳定的算法正好反了,)

     

     

     

     

    总:

    比较排序和非比较排序

          常见的排序算法都是比较排序,非比较排序包括计数排序、桶排序和基数排序,非比较排序对数据有要求,因为数据本身包含了定位特征,所有才能不通过比较来确定元素的位置。

          比较排序的时间复杂度通常为O(n2)或者O(nlogn),比较排序的时间复杂度下界就是O(nlogn),而非比较排序的时间复杂度可以达到O(n),但是都需要额外的空间开销。

    • 若n较小(数据规模较小),插入排序或选择排序较好
    • 若数据初始状态基本有序(正序),插入、冒泡或随机快速排序为宜
    • 若n较大,则采用时间复杂度为O(nlogn)的排序方法:快速排序或堆排序
    • 快速排序是目前基于比较的排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
    • 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。

     

     

     

     

     

     

     

     

     

    展开全文
  • 数据结构——学习笔记——入门必看【建议收藏】

    万次阅读 多人点赞 2020-02-09 17:34:57
    什么是数据结构: 是数据之间存在一种或多种特定关系的数据元素集合 为编写出一个“好”的程序,必须分析待处理对象的特性及各处理对象之间存在的关系 这也就是研究数据结构的意义所在 第一章:数据结构绪论 1....

    终身受益的700个网站【史上最全】——建议收藏“卑微的小丑”总有一个可以帮助你!!!

    https://blog.csdn.net/liu17234050/article/details/105092333

    目录:

    第一章: 数据结构绪论

    1.什么是程序

    2.逻辑结构&物理结构的区别用法

    3.顺序存储&链式存储的区别用法

    第二章:算法

    1.定义

    2.特性

    3.算法时间效率度量

    4.时间复杂度&空间复杂度的区别用法

    第三章:线性表

    1.定义

    2.顺序存储结构&链式存储结构的区别和用法

    第四章:栈与队列

    1.什么是栈

    (1) 栈顶(top)

    (2)栈底(bottom)

    (3) 空栈

    2. 顺序栈&链栈

    (1) 顺序栈

    (2)链栈

    (3)两者示意图如下所示:

    (4)顺序栈&链栈的异同

    A:同【时间复杂度】

    B:异【空间性能】

    3.栈的内部实现原理

    4.递归

    (1) 定义

    (2)条件:

    (3)斐波那契数列(Fibonacci)

    5.栈的数学表达式的求值

    6.队列(queue)

    (1)定义 

    (2)对头

    (3)队尾

    第五章:数组和广义表

    第六章:串【字符串】

    1.定义

    2.串的逻辑结构

    3.串的存储结构

    (1)串的顺序存储结构

    (2)串的链式存储结构

    第七章:树

    1.定义

    2.特点【根节点-子树-子节点】

    3.线性结构&树结构的区别用法

    4.二叉树

    (1)定义

    (2)特点

    (3)五种基本形态

    (4)五种特性【理解】

    (5)二叉树的四种遍历方式【前序遍历-中序遍历-后序遍历-层序遍历】

    (6) 赫夫曼编码

    A:定义

    a: 路径和路径长度

    b: 结点的权及带权路径长度

    c: 树的带权路径长度

    补充:数据结构——哈夫曼(Huffman)树+哈夫曼编码

    第八章:图(Graph)

    1:定义

     2:线性表&树&图的【数据元素名称-有无结点-内部之间的关系】的区别

    3:图的五种种类【无向图-有向图-简单图-完全无向图-有向完全图】

    4:权

    (1)定义

    (2)图形化解释

    5.环

    (1)定义

    (2)图形化解释

    6.特性

    A: 连通

    B:简单路径

    C:强连通图

    D: 强连通分量

    E:生成树

    F:有向树

    G:森林

    7.图的两种遍历【深度优先遍历-广度优先遍历】的区别用法

    8.最小生成树连通网的两种算法区别和用法【普里姆(Prim)算法-克鲁斯卡尔(Kruskal)算法】

     9. AOV 网(Activity On Vertext Network)

    10.拓扑序列

    对 AOV 网进行拓扑排序的基本思路是:

    11: AOE 网(Activity On Edge Network)  

    A:始点或源点

    B:终点或汇点

    C:路径长度

    D:关键路径

    E:关键活动 

    第九章:动态内存管理

    第十章:查找

    1.查找表(Search Table)

    2.关键字(Key)

    (1)键值

    (2)关键码

    (3)关键字(Primary Key)

    (4)次关键字(Secondary Key)

    3.查找

    (1)定义

    (2)静态查找表&动态查找表的区别用法【顺序查找-线性查找-折半查找-二分查找-有序表查找-插值查找-斐波那契查找-索引顺序表查找-分块查找】

    4.索引

    (1)定义 

    (2)按照结构分类

    (3)三种线性索引&两种表【稠密索引-分块索引-倒排索引-多重表-倒排表】

    5.二叉排序树【 二叉查找树】

    (1)定义

    (2)目的

    (3)存储方式

    A:定义

    B:执行插入或删除操作时

    C:查找操作时

    D:查找次数最少

    E:查找次数最多

    F:二叉排序树的形状

    G:解决二叉排序树的形状问题

    H:深度

    I:时间复杂度

    J:平衡二叉树(AVL 树)

    (4)平衡二叉树

    (5)平衡因子 BF(Balance Factor)

    (6)平衡二叉树

    (7)最小不平衡子树

    (8)平衡二叉树实现原理

    6.多路查找树(muitl-way search tree)

    (1)分析

     (2)定义

    (3)四中常见的多路查找树【2-3 树$2-3-4 树$B 树$B+ 树】的区别用法

    7.散列技术

    (1)定义

    (2)散列表【哈希表(Hash table)】

    (3)散列地址

    (4)散列函数设计原则

    A:计算简单:

    B:散列地址分布均匀:

    (5)构造散列函数的六种方法【直接定址法-数字分析法-平方取中法-折叠法-除留余数法-随机数法】

    (6)处理散列冲突的四种方法【开放定址法-再散列函数法-链地址法-公共溢出区法】

    (7)散列表性能分析

    A:散列函数是否均匀:

    B:处理冲突的方法:

    C:散列表的装填因子:

    第十一章:内部排序

    1.定义

    2.多个关键的排序最终都可以转化为单个关键字的排序

    3.排序的稳定性

    4.内排序&外排序

    (1)内排序

    (2)外排序

    5.影响内排序的三个方面

    (1)时间性能

    A:比较 

    B:移动 

    (2)辅助空间

    (3)算法的复杂性

    6.C 语言中的十四种内部排序算法

    第十二章:外部排序

    1.外部排序定义

    2.外部排序的基本思路

    举例:

    3.外部排序算法

    (1)定义

    (2)外部排序算法由两个阶段构成

    (3)2-路平衡归并

    举例: 

    (4)5-路归并

    4.多路平衡归并排序(胜者树、败者树)算法

    (1)引入

    (2)败者树实现内部归并

    (3)胜者树

    (4)败者树

    (5)败者树内部归并的具体实现

    5.置换-选择排序算法

    1.引入

    (2)图解

    (3)操作过程

    (4)置换选择排序算法的具体实现

    6.最佳归并树

    (1)思考

    (2)举例参考

    7.“虚段”的归并树

    (1)引入

    (2)如何判断是否需要增加虚段,以及增加多少虚段

    十三:文件




    ==================================================================================

    参考文件:

    《大话数据结构课本》

    《[数据结构课本(C语言版)].严蔚敏_吴伟民.扫描版》

    《数据结构与算法分析课本》

     

    参考书籍(word+ppt版本)地址:

    链接:https://pan.baidu.com/s/1725YDUPVfIXRl8U4QnY0pA 
    
    提取码:750y

     

    参考视频地址:

    【C语言描述】《数据结构和算法》(小甲鱼)99集:推荐学习-《推荐》

    https://www.bilibili.com/video/av21828275/


     

    【青岛大学-王卓】数据结构与算法基础(40个小时)  173集:推荐学习-《推荐》

    https://www.bilibili.com/video/av82837069?from=search&seid=16668239479775468624

     

    ==================================================================================

    什么是数据结构:

    是数据之间存在一种或多种特定关系的数据元素集合

    为编写出一个“好”的程序,必须分析待处理对象的特性及各处理对象之间存在的关系

    这也就是研究数据结构的意义所在

     

    知识结构图:

    https://www.processon.com/view/5cf5d469e4b0da05395d5143#map

    第一章: 数据结构绪论

    1.什么是程序

    程序 = 数据结构 + 算法 

    2.逻辑结构&物理结构的区别用法

    https://blog.csdn.net/liu17234050/article/details/104251163

    3.顺序存储&链式存储的区别用法

    https://blog.csdn.net/liu17234050/article/details/104251282

    ==================================================================================

    第二章:算法

    1.定义

    算法是解决特定问题求解步骤的描述

    在计算机中表现为指令的有限序列

    并且每条指令表示一个或多个操作

     

     简而言之,算法是描述解决问题的方法

    2.特性

    输入、输出、有穷性、确定性和可行性

    好的算法:应该具有正确性,可读性,健壮性,高效率和低存储量的特征

    3.算法时间效率度量

    (1)可以忽略加法常数

    O(2n + 3) = O(2n)

    (2)与最高次项相乘的常数可忽略

    O(2n^2) = O(n^2)

    (3) 最高次项的指数大的,函数随着 n 的增长,结果也会变得增长得更快

    O(n^3) > O(n^2)

    (4)判断一个算法的(时间)效率时,函数中常数和其他次要项常常可以忽略,而更应该关注主项(最高阶项)的阶数

    O(2n^2) = O(n^2+3n+1)
    O(n^3) > O(n^2)

    4.时间复杂度&空间复杂度的区别用法

    https://blog.csdn.net/liu17234050/article/details/104251507

    ==================================================================================

    第三章:线性表

    1.定义

    零个或多个数据元素的有限序列

    2.顺序存储结构&链式存储结构的区别和用法

    https://blog.csdn.net/liu17234050/article/details/104252533

    ==================================================================================

    第四章:栈与队列

    1.什么是栈

    是限定仅在表尾(栈顶)进行插入和删除操作线性表

    栈又称为 后进先出(Last In First Out) 的线性表,简称 LIFO 结构

    (1) 栈顶(top)

    我们把允许插入和删除的一端称为 栈顶

    (2)栈底(bottom)

    另一端称为 栈底

    (3) 空栈

    不含任何任何数据元素的栈称为 空栈

    2. 顺序栈&链栈

     是线性表的特例,其具备先进后出 FILO 特性

    (1) 顺序栈

    可以使用线性表的顺序存储结构(即数组)实现栈,将之称之为 顺序栈

    (2)链栈

    可以使用单链表结构实现栈,将之称之为 链栈

    (3)两者示意图如下所示:

    (4)顺序栈&链栈的异同

    A:同【时间复杂度】

    顺序栈和链栈的时间复杂度均为

    B:异【空间性能】

    a:顺序栈

    顺序栈需要事先确定一个固定的长度(数组长度)

    可能存在内存空间浪费问题,但它的优势存取时定位很方便

    b:链栈

    要求每个元素都要配套一个指向下个结点的指针域

    增大了内存开销,但好处是栈的长度无限

    因此,如果栈的使用过程中元素变化不可预料,有时很小,有时很大,那么最好使用链栈

    反之,如果它的变化在可控范围内,则建议使用顺序栈

    3.栈的内部实现原理

    栈的内部实现原理其实就是数组或链表的操作

    而之所以引入 这个概念,是为了将程序设计问题模型化

    用高层的模块指导特定行为(栈的先进后出特性),划分了不同关注层次,使得思考范围缩小

    更加聚焦于我们致力解决的问题核心,简化了程序设计的问题

    4.递归

    (1) 定义

    在运行的过程中调用自己

    每个递归定义必须至少有一个条件,使得当满足条件时,递归不再进行

    (2)条件:

    1. 子问题须与原始问题为同样的事,且更为简单;

    2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理

    (3)斐波那契数列(Fibonacci)

    指的是这样一个数列:1、1、2、3、5、8、13、21、……,即当前位置的值为前面两项之和

    用数学表达式表达如下:

    如果我们直接将按上面的公式用代码进行翻译,如下所示:

    int fbi(const int n){
        int i;
        int ret;
        int last1 = 0;
        int last2 = 1;
        if (n == 0){
            ret = 0;
        }else if (n == 1){
            ret = 1;
        }else{
            for(i = 2; i <= n; ++i){
                ret = last1 + last2;
                last1 = last2;
                last2 = ret;
            }
        }
        return ret;
    }

    但是如果我们使用递归方式实现,则会更加简洁:

    int fbi(const int n){
        if (n < 2){
            return n == 0 ? 0 : 1;
        }
        return fbi(n-1) + fbi(n-2);
    }

    使用递归,简洁之余,还更加契合其数学公式的定义

    5.栈的数学表达式的求值

    其原理是通过将 中缀表达式(即标准的四则运算表达式) 以特定操作进行进栈出栈操作

    得到一个对应后缀表达式(也称为逆波兰(Reverse Polish Notation,PRN)表示)

    然后将该后缀表达式再次通过特定操作进行出栈入栈操作,即可得到运算结果

    6.队列(queue)

    (1)定义 

    是只允许在一端进行插入操作,而在另一端进行删除操作线性表

    队列 是一种 先进先出(First In First Out) 的线性表

    (2)对头

    允许删除的一端称为对头

    (3)队尾

    允许插入的一端称为队尾

    线性表有顺序存储和链式存储,栈是线性表,所以有这两种存储方式

    同样,队列作为一种特殊的线性表也同样存在这两种存储方式

    ==================================================================================

    第五章:数组和广义表

    学习地址:

    http://c.biancheng.net/data_structure/array_list/

    ==================================================================================

    第六章:串【字符串】

    1.定义

    串(string) 是由零个或多个字符组成有限序列,又名叫 字符串

    2.串的逻辑结构

    的逻辑结构和线性表很相似

    不同之处在于串针对的是字符集

    也就是串中的元素都是字符

     

    因此,对于串的基本操作与线性表是有很大差别的

    线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素

    但串中更多的是查找子串位置,得到指定位置子串,替换子串等操作

    3.串的存储结构

     的存储结构线性表相同,分为两种

    (1)串的顺序存储结构

    串的顺序存储结构是用 一组地址连续的存储单元 来存储串中的字符序列。一般是用定长数组来定义

    由于是定长数组,因此就会存在一个预定义的最大串长度

    一般可以将实际的串长度值保存在数组 0 下标位置,也可以放在数组最后一个下标位置

    也有些语言使用在串值后面加一个不计入串长度的结束标记符(比如\0

    来表示串值得终结,这样就无需使用数字进行记录

    (2)串的链式存储结构

    对于串的链式存储结构,与线性表是相似

    但由于串结构的特殊性(结构中的每个元素数据都是一个字符

    如果也简单地将每个链结点存储一个字符,就会存在很大的空间浪费

     

    因此,一个结点可以考虑存放多个字符

    如果最后一个结点未被占满时,可以使用 "#" 或其他非串值字符补全

    串的链式存储结构除了在链接串与串操作时有一定的方便之外

    总的来说不如顺序存储灵活,性能也不如顺序存储结构好

    ==================================================================================

    第七章:树

    1.定义

    树是 结点的有限集

      时称为空树

     其实也是一种递归的实现,即树的定义之中还用到了树的概念

    2.特点【根节点-子树-子节点】

    在任意一棵非空树中:

    (1)且仅有一个特定的结点根结点(Root)

    (2)当  时,其余结点可分为互不相交的有限集

    其中每一个集合本身又是一棵树,并且称为根的 子树(SubTree)

    其图示如下所示:

    (3)根结点大于 0 时根结点是唯一的不可能同时存在多个根结点

    (4)子结点大于 0时,子树的个数没有限制,但它们一定是互不相交

    下图所示的结构就不符合树的定义,因为它们都有相交的子树:

    3.线性结构&树结构的区别用法

    https://blog.csdn.net/liu17234050/article/details/104256016

    单独使用顺序存储结构(即数组)无法很好地实现树的存储概念,不过如果充分利用顺序存储和链式存储结构的特点,则完全可以实现对数的存储结构的表示

    4.二叉树

    (1)定义

    二叉树(Binary Tree):是  个结点的有限集合

    该集合或者为空集(称为空二叉树

    或者由一个根结点和两棵互不相交的

    分别称为根结点的左子树右子树的二叉树组成

    (2)特点

    A:每个结点最多只能有两棵子树

    B:左子树和右子树是有顺序的,次序不能任意颠倒

    C: 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树

    (3)五种基本形态

    A:空二叉树

    B:只有一个跟结点

    C:根结点只有左子树

    D:根结点只有右子树

    E:根结点既有左子树又有右子树

    (4)五种特性【理解

    A:在二叉树的第 层上至多有 个结点

    比如第一层是根结点,只有一个;第二层有两个:根结点的左子树和右子树···

     

    B:深度的二叉树至多有结点

    比如深度为 1,则至多只有 1 个结点,即根结点;深度为 2,则至多只有 3 个结点:根结点,根结点的左子树,根结点的右子树···

     

    C:对任何一棵二叉树如果其叶子结点点数为 度(即子结点数)为 2 结点数为 ,则 

     

    D:具有个结点的完全二叉树深度表示不大于的最大整数

     

    E:如果对一棵树有 个结点的完全二叉树(其深度的结点按层序编号(从第 1 层到第层,每层从左到右),对任一结点 有:

    a:.如果 ,则结点 是二叉树的根,无双亲;如果则其双亲是结点

    b:如果,则结点  无左孩子(结点为叶子结点);否则其左孩子是结点

    c:如果则结点 无右孩子;否则其右孩子是结点

    前面提及到顺序存储对数这种一对多的关系结构实现起来是比较困难的

    但是对于二叉树,由于它的特殊性,使得用顺序存储结构也可以实现

    (5)二叉树的四种遍历方式【前序遍历-中序遍历-后序遍历-层序遍历】

    https://blog.csdn.net/liu17234050/article/details/104256408

    (6) 赫夫曼编码

    树,森林看似复杂

    其实它们都可以转化为简单的二叉树来处理

    这样就使得面对树和森林的数据结构时,编码实现成为了可能

    最基本的压缩编码方法:赫夫曼编码

    A:定义

    给定n个权值作为n个叶子结点

    构造一棵二叉树,若树的带权路径长度达到最小,则这棵树被称为哈夫曼树

     

    a: 路径和路径长度

    定义:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径

    通路中分支的数目称为路径长度

    若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。  

    例子:100和80的路径长度是1,
    50和30的路径长度是2,
    20和10的路径长度是3。

     

    b: 结点的权及带权路径长度

    定义:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的

    结点的带权路径长度为:从根结点结点之间路径长度与该结点的权的乘积

    例子:节点20的路径长度是3,
    它的带权路径长度= 路径长度 * 权 = 3 * 20 = 60。

     

    c: 树的带权路径长度

    定义:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。 

     例子:示例中,树的WPL= 1*100 + 2*50 + 3*20 + 3*10 = 100 + 100 + 60 + 30 = 290。

     

    比较下面两棵树:

    上面的两棵树都是以{10, 20, 50, 100}为叶子节点的树。

    左边的树WPL=2*10 + 2*20 + 2*50 + 2*100 = 360  右边的树WPL=350

    左边的树WPL > 右边的树的WPL。

    你也可以计算除上面两种示例之外的情况,但实际上右边的树就是{10,20,50,100}对应的哈夫曼树

    补充:数据结构——哈夫曼(Huffman)树+哈夫曼编码

    ==================================================================================

    第八章:图(Graph)

    1:定义

    顶点的有穷非空集合和顶点之间边的集合组成

    通常表示为

    其中, 表示一个

     是图  中的顶点的集合

     是图  边的集合

    A:在线性表中

    数据元素之间是被串起来的仅有线性关系

    每个数据元素只有一个直接前驱和一个直接后驱


    B:在树形结构中

    数据元素之间有着明显的层次关系

    并且每一层上的数据元素可能和下一层中多个元素相关

    但只能和上一层中一个元素相关


    C:是一种较线性表更加复杂的数据结构

    在图形结构中,结点之间的关系可以是任意的

    图中任意两个数据元素之间都可能相关

     2:线性表&树&图的【数据元素名称-有无结点-内部之间的关系】的区别

    https://blog.csdn.net/liu17234050/article/details/104256952

    3:图的五种种类【无向图-有向图-简单图-完全无向图-有向完全图】

    https://blog.csdn.net/liu17234050/article/details/104258272

    4:

    (1)定义

    有些图的边或弧具有与它相关的数字,这种 与图的边或弧相关的数叫做权(Weight)

    这些权可以表示从一个顶点到另一个顶点的距离或耗费

    这种带权的图通常称为网(Network)

    (2)图形化解释

    图就是一张带权的图

    即标识中国四大城市的直线距离的网

    此图中的就是两地的距离

    图结构中,路径的长度是路径上的边或弧的数据

     

    第一个顶点最后一个顶点相同的路径称为 回环 或 (Cycle)

     

    序列中顶点不重复出现的路径称为 简单路径

    5.环

    (1)定义

    除了第一个顶点和最后一个顶点之外

    其余顶点不重复出现的回路,称为 简单回路 或 简单环

    (2)图形化解释

    下图所示两个图粗线都构成环

    左侧的环只有第一个顶点和最后一个顶点都是 B

    其余顶点没有重复出现,因此其是一个简单环

    右侧的环,由于顶点 C 的重复

    因此它就不是简单环了

    6.特性

    A: 连通

    图中顶点间存在 路径,两顶点存在路径则说明是 连通 的

     

    B:简单路径

    如果路径最终回到起始点则成为 ,当中不重复叫 简单路径

     

    C:强连通图

    若任意两顶点都是连通的,则图就是 连通图有向则称为 强连通图

     

    D: 强连通分量

    图中有子图,若子图极大连通则就是 连通分量有向的则称为 强连通分量

    E:生成树

    无向图连通顶点叫 生成树

     

    F:有向树

    有向图中一顶点入度0

    其余顶点入度为1的叫 有向树

     

    G:森林

    一个有向图由若干棵有向树构成生成 森林

    由于图的结构比较复杂,任意两个顶点之间都可能存在联系

    因此无法以数据元素在内存中的物理位置来表示元素之间的关系

    也就是说,不可能用简单的顺序存储结构(即数组)来表示

     

    而多重链表尽管可以实现图结构(即以一个数据域和多个指针域组成的结点表示图中的一个顶点)

    是却存在内存浪费或操作不便的问题

     

    因此,图存储结构最终还是得通过结合顺序存储链式存储才能做到比较好地实现

    7.图的两种遍历【深度优先遍历-广度优先遍历】的区别用法

    https://blog.csdn.net/liu17234050/article/details/104258892

    8.最小生成树连通网的两种算法区别和用法【普里姆(Prim)算法-克鲁斯卡尔(Kruskal)算法】

    https://blog.csdn.net/liu17234050/article/details/104262387

     9. AOV 网(Activity On Vertext Network)

    在一个表示工程的有向图中

    顶点表示活动

    表示活动之间的优先关系

     

    这样的有向图为顶点表示活动的网,我们成为 AOV 网(Activity On Vertext Network)

    AOV 网中的弧表示活动之间存在的某种制约关系,同时 AOV 网中不能存在回路

    10.拓扑序列

    设 是一个具有  个顶点的有向图

     中的顶点序列

    满足若从顶点  有一条路径

    则在顶点序列中顶点 必在顶点之前

     

    则我们将这样的顶点序列称为一个 拓扑序列

     

    所谓 拓扑排序,其实就是对一个有向图构造拓扑序列的过程

    对 AOV 网进行拓扑排序的基本思路是:

    A:从 AOV 网中选择一个入度为 0 顶点输出

    B:然后删去此顶点

    C:删除此顶点为尾的弧

    D:继续重复此步骤

    E:直到输出全部顶点

    或者 AOV 网中不存在入度为 0 的顶点为止

    11: AOE 网(Activity On Edge Network)  

    在一个表示工程的带权有向图中

    顶点表示事件

    有向边表示活动

    用边上的权值表示活动的持续时间

     

    这种有向图的边表示活动的网

    我们称之为 AOE 网(Activity On Edge Network) 

    A:始点或源点

    我们把 AOE 网中没有入边的顶点称为始点或源点

    B:终点或汇点

    没有出边的顶点称为终点或汇点

    由于一个工程,总有一个开始,一个结束,所以正常情况下,AOE 网只有一个源点一个汇点 

    C:路径长度

    我们把路径上各个活动所持续的时间之和称为 路径长度

    D:关键路径

    从源点到汇点具有最大长度的路径叫 关键路径

    E:关键活动 

    关键路径上的活动关键活动 

    ==================================================================================

    第九章:动态内存管理

    学习地址:

    http://c.biancheng.net/data_structure/memory/

    ==================================================================================

    第十章:查找

    1.查找表(Search Table)

    同一类型的数据元素(或记录)构成的集合

    2.关键字(Key)

    (1)键值

    是数据元素中的某个数据项的值,又称为 键值,用它可以标识一个数据元素

    (2)关键码

    也可以标识一个记录的某个数据项(字段),我们称为 关键码

    (3)关键字(Primary Key)

    若此关键字可以唯一地标识一个记录,则称此关键字为主关键字

    (4)次关键字(Secondary Key)

    而对于那些可以识别多个数据元素(或记录)的关键字,我们称为 次关键字

    3.查找

    (1)定义

    就是根据给定的某个值,在查找表中确定一个其关键字等于给定值得数据元素(或记录)

     

    从逻辑上来说,查找所基于的数据结构集合

    集合中的记录之间没有本质关系

    可是要想获得较高的查找性能

    我们就不能不改变数据元素之间的关系

    在存储时可以将查找集合组织成表,树等结构

    比如,对于静态查找表来说,我们不妨应用线性表结构来组织数据

    这样可以使用顺序查找算法如果再对主关键字排序,则可以应用折半查找等技术进行高效的查找

    (2)静态查找表&动态查找表的区别用法【顺序查找-线性查找-折半查找-二分查找-有序表查找-插值查找-斐波那契查找-索引顺序表查找-分块查找】

    https://blog.csdn.net/liu17234050/article/details/104264637

    4.索引

    (1)定义 

    就是把一个关键字与它对应记录相关联的过程(关键字=记录

    数据结构的最终目的就是提高数据的处理速度

    索引是为了加快查找速度而设计的一种数据结构

    一个索引由若干个索引项构成

    每个索引项至少应包含关键字和其对应的记录存储器中的位置等信息

    索引技术是组织大型数据库以及磁盘文件的一种重要技术

    (2)按照结构分类

    索引按照结构可以分为:线性索引,树形索引和多级索引

    (3)三种线性索引&两种表【稠密索引-分块索引-倒排索引-多重表-倒排表】

    https://blog.csdn.net/liu17234050/article/details/104266787

    假设查找的数据集普通的顺序存储

    那么插入操作(插入到末尾)和删除操作(删除记录后记录向前移相对耗时

    如果只把删除的元素与最后一个元素互换

    然后表记录数减一,则很高效)的效率是可以接受的

    但是顺序存储表由于无序会造成查找效率很低
    如果查找的数据集是有序线性表,并且是顺序存储

    那么其查找(二分法,插值法,斐波那契···)效率很高

    但是由于有序,在插入和删除操作上,就需要耗费大量的时间

    5.二叉排序树【 二叉查找树

    而使用 二叉排序树 这种存储结构时,就可以实现对动态查找表的高效插入,删除和查找操作

    (1)定义

    二叉排序树(Binary Sort Tree):又称为 二叉查找树

    它或者是一棵空树

     

    或者是具有下列性质的二叉树:

    A:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值

    B:若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值

    C:它的左、右子树也分别为二叉排序树

    (2)目的

    构造一棵 二叉排序树 的目的,其实并不是为了排序

    而是为了提高查找和插入删除关键字的速度

    不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的

     

    而二叉排序树这种非线性的结构

    也有利于插入和删除的实现

    (3)存储方式

    A:定义

    二叉排序树 是以链接的方式存储

     

    B:执行插入或删除操作

    保持了链接存储结构在执行插入或删除操作不用移动元素的有点(只需修改链接指针)

    插入和删除的时间性能比较好

     

    C:查找操作

    而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径

    其比较次数等于给定值得结点在二叉排序树的层数

     

    D:查找次数最少

    极端情况下,查找次数最少为 1 次

    即根结点就是要找的结点

     

    E:查找次数最多

    最多也不会超过树的深度

    也就是说,二叉排序树的查找性能取决于二叉排序树的形状

     

    F:二叉排序树的形状

    很可惜的是,二叉排序树的形状是不确定的(想象下极端的右斜树或左斜树)

     

    G:解决二叉排序树的形状问题

    对于这个问题,解决方案就是让二叉排序树左右子树是比较平衡的

     

    H:深度

    即其深度完全二叉树相同,均为

     

    I:时间复杂度

    那么查找的时间复杂度就为

     

    J:平衡二叉树(AVL 树)

    近似二分查找,这种结构就称为 平衡二叉树

    (4)平衡二叉树

    (Self-Balancing Binary Search Tree 或 Height-Balanced Binary Search Tree)

    是一种二叉排序树

    其中每一个结点左子树右子树高度差至多等于 1

    (5)平衡因子 BF(Balance Factor)

    将二叉树上结点的左子树深度减去右子树深度的值称为 平衡因子 BF

    (6)平衡二叉树

    上所有结点的平衡因子只可能是 -1、0 和 1

    只要二叉树上有一个结点的平衡因子的绝对值大于 1,则该二叉树就是不平衡的

    (7)最小不平衡子树

    距离插入结点最近的,且平衡因子的绝对值大于 1 结点为根的子树,我们称为 最小不平衡子树

    如上图所示,当新插入结点 37 时

    距离它最近的平衡因子绝对值超过 1 的结点是 58

    (58 的左子树高度为 2,右子树高度为 0)

    所以从 58 开始以下的子树为最小不平衡子树

    (8)平衡二叉树实现原理

    平衡二叉树构建的基本思想就是在构建二叉树排序树过程中

    每当插入一个结点时

    先检查是否因插入而破坏了树的平衡性

    若是,则找出最小不平衡子树

    在保持二叉排序树特性的前提下

    调整最小不平衡子树中各结点之间的链接关系

    进行相应的旋转,使之成为新的平衡子树

    6.多路查找树(muitl-way search tree)

    (1)分析

    对于 来说,一个结点只能存储一个元素

    那么在元素非常多的时候,就会使得要么树的度非常大结点拥有子树的个数的最大值

    要么树的高度非常大,甚至两者都必须足够大才行

    这就使得内存存取外存次数非常多

    这显然成了时间效率上的瓶颈

    这迫使我们要打破每一个结点只存储一个元素的限制,为此引入了多路查找树的概念

     (2)定义

    每一个结点孩子树可以多于两个

    每一个结点处可以存储多个元素

    由于它是查找树,因此所有元素之间存在某种特定的排序关系(即其是有序的)

    (3)四中常见的多路查找树【2-3 树$2-3-4 树$B 树$B+ 树】的区别用法

    https://blog.csdn.net/liu17234050/article/details/104270364

    7.散列技术

    (1)定义

    是在记录的存储位置和它的关键字之间建立一个确定的对应关系

    使得每个关键字 key 对应一个存储位置

    查找时,根据这个确定的对应关系找到给定值 key 的映射

    若查找集合中存在这个记录,则必定在 位置上

    这里我们把这种对应关系 称为 散列函数,又称为 哈希(Hash)函数

     

    散列技术 既是一种存储方法,也是一种查找方法

    通过 散列函数 计算关键字散列地址

    并按此散列地址存储对应记录查找时

    通过同样的 散列函数 计算关键字散列地址,按此地址访问对应记录 

    (2)散列表【哈希表(Hash table)】

    采用散列技术记录存储在一块连续的存储空间中

    则这块连续存储空间称为 散列表 或 哈希表

    (3)散列地址

    那么关键字对应的记录存储位置我们称为 散列地址

    (4)散列函数设计原则

    A:计算简单

    散列函数的计算时间不应该超过其他查找技术与关键字比较的时间

     

    B:散列地址分布均匀

    防止散列冲突最好的办法就是尽量让散列地址均匀地分布在存储空间中

    这样可以保证存储空间的有效利用减少为处理冲突而耗费的时间

    (5)构造散列函数的六种方法【直接定址法-数字分析法-平方取中法-折叠法-除留余数法-随机数法】

    https://blog.csdn.net/liu17234050/article/details/104270709

    (6)处理散列冲突的四种方法【开放定址法-再散列函数法-链地址法-公共溢出区法】

    https://blog.csdn.net/liu17234050/article/details/104271343

    (7)散列表性能分析

    A:散列函数是否均匀

    散列函数的好坏直接影响着出现冲突的频繁程度

    由于不同散列函数同一组随机的关键字,产生冲突的可能性是相同的

    因此可以不考虑它对平均查找长度的影响

     

    B:处理冲突的方法

    相同关键字相同散列函数,但处理冲突方法不同,会使得平均查找长度不同

     

    比如线性探测处理冲突可能会产生堆积

    显然就没有二次探测法好

     

    链地址法处理冲突不会产生任何堆积

    因而具有更佳的平均查找性能 

     

    C:散列表的装填因子

    所谓的装填因子

     标志着散列表的装满的程度

    当填入表中的记录越多

    就越大,产生冲突的可能性就越大

     

    比如假设散列表长度是 12,而填入表中的记录个数为 11,那么此时的装填因子

     

    因此再填入最后一个关键字产生冲突可能性就非常之大

    也就是说,散列表的平均查找长度取决于装填因子

    而不是取决于查找集合中的记录个数

     

    不管记录个数有多大

    我们总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内

    此时我们散列查找的时间复杂度就真的是

     

    通常我们都是将散列表的空间设置得比查找集合

    此时虽然浪费了一定的空间换来的是查找效率的大大提升

    总的来说,还是非常值得的

    ==================================================================================

    第十一章:内部排序

    1.定义

    排序 的依据是关键字之间的大小关

    即对同一个记录集合(数据集合)

    针对不同的关键字进行排序

    可以得到不同的序列

    注:此处的关键字可以是记录的主关键字,也可以是此关键字,甚至是若干数据项的组合

    2.多个关键的排序最终都可以转化为单个关键字的排序

    比如,现在要对学生的成绩进行排序

    排序规则为按总分降序排序,若总分分数相同,按主科(语数英)总分数进行降序排序

    直接的思路肯定是先进行总分排序,然后遇到总分相同的,就进行主科总分排序

     

    但是还可以使用一个技巧来实现一次排序即可完成上述组合排序问题:

    例如,把总分与主科总分当成字符串首尾拼接到一起
    (注意如果主科成绩不够三位,则需在前面补零)
    
    (比如:甲,乙两人的总分都为 753,其中,
    甲的理科总分为 229,
    而乙的理科总分为 236,
    
    则甲的拼接为:"753229",
    乙的拼接为:"753236",
    
    比较这两个拼接字符串即可直接(一次性)
    直到乙排在甲前面(乙 > 甲)

     

    3.排序的稳定性

    假设 

    且在排序前的序列中领先于 

    如果排序后

    则称所用的排序方法是 稳定的

     

    反之,若可能使得排序后的序列中 则称所用的排序方法是 不稳定的

    • 如下图所示:

    4.内排序&外排序

    据在排序过程中待排序的记录是否全部被放置在内存中,排序分为:内排序 和 外排序

    (1)内排序

    是在排序整个过程中,待排序的所有记录全部被放置在内存中

    内排序分为插入排序交换排序选择排序 和 归并排序

     

     

    (2)外排序

    是由于排序的记录个数太多不能同时放置在内存

    整个排序过程需要在内外存之间多次交互数据才能进行

    5.影响内排序的三个方面

    (1)时间性能

    在内排序中,主要进行两种操作比较移动

    A比较 

    是指关键字之间的比较,这是做排序最基本的操作

    B移动 

    是指记录从一个位置移动到另一个位置

    事实上,移动可以通过改变记录的存储方式来予以避免

    高效率的内排序算法应该是具有尽可能少的关键字比较次数尽可能少的记录移动次数

    (2)辅助空间

    辅助内存空间是除了存放待排序所占用的存储空间之外

    执行算法所需要的其他存储空间

    (3)算法的复杂性

    此处指的算法本身的复杂度

    不是指算法的时间复杂度

    6.C 语言中的十四种内部排序算法

    https://blog.csdn.net/liu17234050/article/details/104217658

    ==================================================================================

    第十二章:外部排序

    1.外部排序定义

    外部排序指的是大文件的排序

    即待排序的记录存储在外存储器上

    待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的

    外部排序最常用的算法多路归并排序

    即将原文件分解成多个能够一次性装入内存的部分

    分别把每一部分调入内存完成排序

    然后,对已经排序的子文件进行多路归并排序
     

    2.外部排序的基本思路

    1)按可用内存的大小

    把外存上含有n个记录的文件分成若干个长度为L的子文件

    把这些子文件依次读入内存,并利用有效的内部排序方法对它们进行排序

    再将排序后得到的有序子文件重新写入外存

     

    2)对这些有序子文件逐趟归并

    使其逐渐由小到大,直至得到整个有序文件为止

    举例:

    假设有一个72KB的文件,其中存储了18K个整数,磁盘中物理块的大小为4KB,将文件分成18组,每组刚好4KB

     

    首先通过18次内部排序,把18组数据排好序,得到初始的18个归并段R1~R18,每个归并段有1024个整数。

    然后对这18个归并段使用4路平衡归并排序:

     

    第1次归并:产生5个归并段

    R11 R12 R13 R14 R15

    其中

    R11是由{R1,R2,R3,R4}中的数据合并而来

    R12是由{R5,R6,R7,R8}中的数据合并而来

    R13是由{R9,R10,R11,R12}中的数据合并而来

    R14是由{R13,R14,R15,R16}中的数据合并而来

    R15是由{R17,R18}中的数据合并而来

    把这5个归并段的数据写入5个文件:

    foo_1.dat foo_2.dat foo_3.dat foo_4.dat foo_5.dat
     

    第2次归并:从第1次归并产生的5个文件中读取数据,合并,产生2个归并段

    R21 R22

    其中R21是由{R11,R12,R13,R14}中的数据合并而来

    其中R22是由{R15}中的数据合并而来

    把这2个归并段写入2个文件

    bar_1.dat bar_2.dat

     

    第3次归并:从第2次归并产生的2个文件中读取数据,合并,产生1个归并段

    R31

    R31是由{R21,R22}中的数据合并而来

    把这个文件写入1个文件

    foo_1.dat

    此即为最终排序好的文件。

    3.外部排序算法

    (1)定义

    外部排序算法,即要借助外部存储器对数据进行排序的算法包括置换平衡归并排序算法、置换选择排序算法等

    外部排序算法的实现,其实就是将体积大的数据分割为内存容得下的多份数据

    然后分别使用内部排序算法进行排序最后进行整合

    和内部排序算法不同,外部排序算法的主要影响因素在于读写内存的次数

     

    而当待排序的文件比内存的可使用容量还大时

    文件无法一次性放到内存中进行排序

    需要借助于外部存储器(例如硬盘、U盘、光盘)

    (2)外部排序算法由两个阶段构成

    (1)按照内存大小,将大文件分成若干长度为 l 的子文件(l 应小于内存的可使用容量)

    然后将各个子文件依次读入内存,使用适当的内部排序算法对其进行排序

    排好序的子文件统称为“归并段”或者“顺段”

    将排好序的归并段重新写入外存,为下一个子文件排序腾出内存空间

    (2)对得到的顺段进行合并,直至得到整个有序的文件为止。

    (3)2-路平衡归并

    举例: 

    有一个含有 10000 个记录的文件,但是内存的可使用容量仅为 1000 个记录,毫无疑问需要使用外部排序算法,具体分为两步:

    • 将整个文件其等分为 10 个临时文件(每个文件中含有 1000 个记录),然后将这 10 个文件依次进入内存,采取适当的内存排序算法对其中的记录进行排序,将得到的有序文件(初始归并段)移至外存。
    • 对得到的 10 个初始归并段进行如图 1 的两两归并,直至得到一个完整的有序文件

    • 如图 1所示有 10 个初始归并段到一个有序文件
    • 共进行了 4 次归并,每次都由 m 个归并段得到 ⌈m/2⌉ 个归并段
    • 这种归并方式被称为 2-路平衡归并

    注意:此例中采用了将文件进行等分的操作,还有不等分的算法

    注意:在实际归并的过程中,由于内存容量的限制不能满足同时将 2 个归并段全部完整的读入内存进行归并

    只能不断地取 2 个归并段中的每一小部分进行归并

    通过不断地读数据和向外存写数据,直至 2 个归并段完成归并变为 1 个大的有序文件。

     

    对于外部排序算法来说

    影响整体排序效率的因素主要取决于读写外存的次数

    即访问外存的次数越多,算法花费的时间就越多,效率就越低

     

    计算机中处理数据的为中央处理器(CPU)

    如若需要访问外存中的数据

    只能通过将数据从外存导入内存,然后从内存中获取

    同时由于内存读写速度快,外存读写速度慢的差异,更加影响了外部排序的效率

     

    (4)5-路归并

    对于同一个文件来说,对其进行外部排序时访问外存的次数归并的次数成正比

    即归并操作的次数越多,访问外存的次数就越多

    图 1 中使用的是 2-路平衡归并的方式,举一反三

    还可以使用 3-路归并、4-路归并甚至是 10-路归并的方式,图 2 为 5-路归并的方式:

    对比 图 1 和图 2可以看出

    对于 k-路平衡归并中 k 值得选择,增加 k 可以减少归并的次数

    从而减少外存读写的次数,最终达到提高算法效率的目的

     

    除此之外,一般情况下对于具有 m 个初始归并段进行 k-路平衡归并时

    归并的次数为:s=⌊logk⁡m ⌋(其中 s 表示归并次数)

    从公式上可以判断出,想要达到减少归并次数从而提高算法效率的目的,可以从两个角度实现

    • 增加 k-路平衡归并中的 k 值
    • 尽量减少初始归并段的数量 m,即增加每个归并段的容量

     

    增加 k 值的想法引申出了一种外部排序算法:多路平衡归并算法

    增加数量 m 的想法引申出了另一种外部排序算法:置换-选择排序算法

    4.多路平衡归并排序(胜者树、败者树)算法

    (1)引入

    但是经过计算得知,如果毫无限度地增加 k 值

    虽然减少读写外存数据的次数

    增加内部归并的时间得不偿失

     

    为了避免增加 k 值的过程中影响内部归并的效率

    在进行 k-路归并时可以使用“败者树”来实现

    该方法在增加 k 值时不会影响其内部归并的效率

    (2)败者树实现内部归并

    败者树是树形选择排序的一种变形,本身是一棵完全二叉树

    (3)胜者树

    树形选择排序一节中

    对于无序表{49,38,65,97,76,13,27,49}创建的完全二叉树如图 1 所示

    构建此树的目的是选出无序表中的最小值

    这棵树与败者树好相反,是一棵“胜者树

    因为树中每个非终端结点除叶子结点之外的其它结点)中的值都

    表示的是左右孩子相比较后的较小值(谁最小即为胜者)

     

    例如叶子结点 49 和 38 相对比,由于 38 更小

    所以其双亲结点中的值保留的是胜者 38

    然后用 38 去继续同上层去比较,一直比较到树的根结点

    (4)败者树

    而败者树恰好相反,其双亲结点存储的是左右孩子比较之后的失败者,而胜利者则继续同其它的胜者去比较

    胜者树和败者树的区别就是:

    胜者树中的非终端结点中存储的是胜利的一方

    败者树中的非终端结点存储的是失败的一方

    而在比较过程中,都是拿胜者去比较

    如图 2 所示为一棵 5-路归并的败者树

    其中 b0—b4 为树的叶子结点

    分别为 5 个归并段中存储的记录的关键字

     ls 为一维数组,表示的是非终端结点,其中存储的数值表示第几归并段(例如 b0 为第 0 个归并段)

    ls[0] 中存储的为最终的胜者,表示当前第 3 归并段中的关键字最小

     

    当最终胜者判断完成后

    只需要更新叶子结点 b3 的值,即导入关键字 15

    然后让该结点不断同其双亲结点所表示的关键字进行比较

    败者留在双亲结点中,胜者继续向上比较

     

    注意:为了防止在归并过程中某个归并段变为空

    处理的办法为

    可以在每个归并段最后附加一个关键字最大值的记录

    这样当某一时刻选出的冠军为最大值时

    表明 5 个归并段已全部归并完成

    (因为只要还有记录,最终的胜者就不可能是附加的最大值)

    (5)败者树内部归并的具体实现

    #include <stdio.h>
    #define k 5
    #define MAXKEY 10000
    #define MINKEY -1
    typedef int LoserTree[k];//表示非终端结点,由于是完全二叉树,所以可以使用一维数组来表示
    typedef struct {
        int key;
    }ExNode,External[k+1];
    External b;//表示败者树的叶子结点
    //a0-a4为5个初始归并段
    int a0[]={10,15,16};
    int a1[]={9,18,20};
    int a2[]={20,22,40};
    int a3[]={6,15,25};
    int a4[]={12,37,48};
    //t0-t4用于模拟从初始归并段中读入记录时使用
    int t0=0,t1=0,t2=0,t3=0,t4=0;
    //沿从叶子结点b[s]到根结点ls[0]的路径调整败者树
    void Adjust(LoserTree ls,int s){
        int t=(s+k)/2;
        while (t>0) {
            //判断每一个叶子结点同其双亲结点中记录的败者的值相比较,调整败者的值,其中 s 一直表示的都是胜者
            if (b[s].key>b[ls[t]].key) {
                int swap=s;
                s=ls[t];
                ls[t]=swap;
            }
            t=t/2;
        }
        //最终将胜者的值赋给 ls[0]
        ls[0]=s;
    }
    //创建败者树
    void CreateLoserTree(LoserTree ls){
        b[k].key=MINKEY;
        //设置ls数组中败者的初始值
        for (int i=0; i<k; i++) {
            ls[i]=k;
        }
        //对于每一个叶子结点,调整败者树中非终端结点中记录败者的值
        for (int i=k-1; i>=0; i--) {
            Adjust(ls, i);
        }
    }
    //模拟从外存向内存读入初始归并段中的每一小部分
    void input(int i){
        switch (i) {
            case 0:
                if (t0<3) {
                    b[i].key=a0[t0];
                    t0++;
                }else{
                    b[i].key=MAXKEY;
                }
                break;
            case 1:
                if (t1<3) {
                    b[i].key=a1[t1];
                    t1++;
                }else{
                    b[i].key=MAXKEY;
                }
                break;
            case 2:
                if (t2<3) {
                    b[i].key=a2[t2];
                    t2++;
                }else{
                    b[i].key=MAXKEY;
                }
                break;
            case 3:
                if (t3<3) {
                    b[i].key=a3[t3];
                    t3++;
                }else{
                    b[i].key=MAXKEY;
                }
                break;
            case 4:
                if (t4<3) {
                    b[i].key=a4[t4];
                    t4++;
                }else{
                    b[i].key=MAXKEY;
                }
                break;
            default:
                break;
        }
    }
    //败者树的建立及内部归并
    void K_Merge(LoserTree ls){
        //模拟从外存中的5个初始归并段中向内存调取数据
        for (int i=0; i<=k; i++) {
            input(i);
        }
        //创建败者树
        CreateLoserTree(ls);
        //最终的胜者存储在 is[0]中,当其值为 MAXKEY时,证明5个临时文件归并结束
        while (b[ls[0]].key!=MAXKEY) {
            //输出过程模拟向外存写的操作
            printf("%d ",b[ls[0]].key);
            //继续读入后续的记录
            input(ls[0]);
            //根据新读入的记录的关键字的值,重新调整败者树,找出最终的胜者
            Adjust(ls,ls[0]);
        }
    }
    int main(int argc, const char * argv[]) {
        LoserTree ls;
        K_Merge(ls);
        return 0;
    }

    运行结果:

    6 9 10 12 15 15 16 18 20 20 22 25 37 40 48

    5.置换-选择排序算法

    1.引入

    上一节介绍了增加 k-路归并排序中的 k 值来提高外部排序效率的方法

    而除此之外,还有另外一条路可走

    即减少初始归并段的个数,也就是本章第一节中提到的减小 m 的值

    m 的求值方法为:m=⌈n/l⌉(n 表示为外部文件中的记录数,l 表示初始归并段中包含的记录数)

    如果要想减小 m 的值,在外部文件总的记录数 n 值一定的情况下,只能增加每个归并段中所包含的记录数 l

    而对于初始归并段的形成,就不能再采用上一章所介绍的内部排序的算法

    因为所有的内部排序算法正常运行的前提是所有的记录都存在于内存中

    而内存的可使用空间是一定的,如果增加 l 的值,内存是盛不下的

    (2)图解

    例如已知初始文件中总共有 24 个记录,假设内存工作区最多可容纳 6 个记录

    按照之前的选择排序算法最少也只能分为 4 个初始归并段

    而如果使用置换—选择排序

    可以实现将 24 个记录分为 3 个初始归并段,如图 1 所示:

    (3)操作过程

     

    置换—选择排序算法的具体操作过程为:
     

    1. 首先从初始文件中输入 6 个记录到内存工作区中;
    2. 从内存工作区中选出关键字最小的记录,将其记为 MINIMAX 记录;
    3. 然后将 MINIMAX 记录输出到归并段文件中;
    4. 此时内存工作区中还剩余 5 个记录,若初始文件不为空,则从初始文件中输入下一个记录到内存工作区中;
    5. 从内存工作区中的所有比 MINIMAX 值大的记录中选出值最小的关键字的记录,作为新的 MINIMAX 记录;
    6. 重复过程 3—5,直至在内存工作区中选不出新的 MINIMAX 记录为止,由此就得到了一个初始归并段;
    7. 重复 2—6,直至内存工作为空,由此就可以得到全部的初始归并段。

     

    拿图 1 中的初始文件为例,首先输入前 6 个记录到内存工作区,其中关键字最小的为 29,所以选其为 MINIMAX 记录,同时将其输出到归并段文件中,如下图所示:

    此时初始文件不为空,所以从中输入下一个记录 14 到内存工作区中,然后从内存工作区中的比 29 大的记录中,选择一个最小值作为新的 MINIMAX 值输出到 归并段文件中,如下图所示:

    初始文件还不为空,所以继续输入 61 到内存工作区中,从内存工作区中的所有关键字比 38 大的记录中,选择一个最小值作为新的 MINIMAX 值输出到归并段文件中,如下图所示:

    如此重复性进行,直至选不出 MINIMAX 值为止,如下图所示:

    当选不出 MINIMAX 值时,表示一个归并段已经生成

    则开始下一个归并段的创建,创建过程同第一个归并段一样

     

    在上述创建初始段文件的过程中,需要不断地在内存工作区中选择新的 MINIMAX 记录

    即选择不小于旧的 MINIMAX 记录的最小值

    此过程需要利用“败者树”来实现



    同上一节所用到的败者树不同的是,在不断选择新的 MINIMAX 记录时

    为了防止新加入的关键字值小的的影响

    每个叶子结点附加一个序号位

    当进行关键字的比较时,先比较序号,序号小的为胜者

    序号相同的关键字值小的为胜者


    (4)置换选择排序算法的具体实现

    #include <stdio.h>
    #define MAXKEY 10000
    #define RUNEND_SYMBOL 10000 // 归并段结束标志
    #define w 6     // 内存工作区可容纳的记录个数
    #define N 24    // 设文件中含有的记录的数量
    typedef int KeyType; // 定义关键字类型为整型
    // 记录类型
    typedef struct{
        KeyType key; // 关键字项
    }RedType;
    typedef int LoserTree[w];// 败者树是完全二叉树且不含叶子,可采用顺序存储结构
    typedef struct
    {
        RedType rec; /* 记录 */
        KeyType key; /* 从记录中抽取的关键字 */
        int rnum; /* 所属归并段的段号 */
    }RedNode, WorkArea[w];
    // 从wa[q]起到败者树的根比较选择MINIMAX记录,并由q指示它所在的归并段
    void Select_MiniMax(LoserTree ls,WorkArea wa,int q){
        int p, s, t;
    // ls[t]为q的双亲节点,p作为中介
       
        for(t = (w+q)/2,p = ls[t]; t > 0;t = t/2,p = ls[t]){
            // 段号小者 或者 段号相等且关键字更小的为胜者
            if(wa[p].rnum < wa[q].rnum || (wa[p].rnum == wa[q].rnum && wa[p].key < wa[q].key)){
                s=q;
                q=ls[t]; //q指示新的胜利者
                ls[t]=s;
            }
        }
        ls[0] = q; // 最后的冠军
    }
    //输入w个记录到内存工作区wa,建得败者树ls,选出关键字最小的记录,并由s指示其在wa中的位置。
    void Construct_Loser(LoserTree ls, WorkArea wa, FILE *fi){
        int i;
        for(i = 0; i < w; ++i){
            wa[i].rnum = wa[i].key = ls[i] = 0;
        }
        for(i = w - 1; i >= 0; --i){
            fread(&wa[i].rec, sizeof(RedType), 1, fi);// 输入一个记录
            wa[i].key = wa[i].rec.key; // 提取关键字
            wa[i].rnum = 1; // 其段号为"1"
            Select_MiniMax(ls,wa,i); // 调整败者树
        }
    }
    // 求得一个初始归并段,fi为输入文件指针,fo为输出文件指针。
    void get_run(LoserTree ls,WorkArea wa,int rc,int *rmax,FILE *fi,FILE *fo){
        int q;
        KeyType minimax;
        // 选得的MINIMAX记录属当前段时
        while(wa[ls[0]].rnum == rc){
            q = ls[0];// q指示MINIMAX记录在wa中的位置
            minimax = wa[q].key;
            // 将刚选得的MINIMAX记录写入输出文件
            fwrite(&wa[q].rec, sizeof(RedType), 1, fo);
            // 如果输入文件结束,则虚设一条记录(属"rmax+1"段)
            if(feof(fi)){
                wa[q].rnum = *rmax+1;
                wa[q].key = MAXKEY;
            }else{ // 输入文件非空时
                // 从输入文件读入下一记录
                fread(&wa[q].rec,sizeof(RedType),1,fi);
                wa[q].key = wa[q].rec.key;// 提取关键字
                if(wa[q].key < minimax){
                    // 新读入的记录比上一轮的最小关键字还小,则它属下一段
                    *rmax = rc+1;
                    wa[q].rnum = *rmax;
                }else{
                    // 新读入的记录大则属当前段
                    wa[q].rnum = rc;
                }
            }
            // 选择新的MINIMAX记录
            Select_MiniMax(ls, wa, q);
        }
       
    }
    //在败者树ls和内存工作区wa上用置换-选择排序求初始归并段
    void Replace_Selection(LoserTree ls, WorkArea wa, FILE *fi, FILE *fo){
        int rc, rmax;
        RedType j;
        j.key = RUNEND_SYMBOL;
        // 初建败者树
        Construct_Loser(ls, wa, fi);
        rc = rmax =1;//rc指示当前生成的初始归并段的段号,rmax指示wa中关键字所属初始归并段的最大段号
       
        while(rc <= rmax){// "rc=rmax+1"标志输入文件的置换-选择排序已完成
            // 求得一个初始归并段
            get_run(ls, wa, rc, &rmax, fi, fo);
            fwrite(&j,sizeof(RedType),1,fo);//将段结束标志写入输出文件
            rc = wa[ls[0]].rnum;//设置下一段的段号
        }
    }
    void print(RedType t){
        printf("%d ",t.key);
    }
    int main(){
        RedType a[N]={51,49,39,46,38,29,14,61,15,30,1,48,52,3,63,27,4,13,89,24,46,58,33,76};
        RedType b;
        FILE *fi,*fo; //输入输出文件
        LoserTree ls; // 败者树
        WorkArea wa; // 内存工作区
        int i, k;
        fo = fopen("ori","wb"); //准备对 ori 文本文件进行写操作
        //将数组 a 写入大文件ori
    fwrite(a, sizeof(RedType), N, fo);
        fclose(fo); //关闭指针 fo 表示的文件
        fi = fopen("ori","rb");//准备对 ori 文本文件进行读操作
        printf("文件中的待排序记录为:\n");
        for(i = 1; i <= N; i++){
            // 依次将文件ori的数据读入并赋值给b
            fread(&b,sizeof(RedType),1,fi);
            print(b);
        }
        printf("\n");
        rewind(fi);// 使fi的指针重新返回大文件ori的起始位置,以便重新读入内存,产生有序的子文件。
        fo = fopen("out","wb");
        // 用置换-选择排序求初始归并段
        Replace_Selection(ls, wa, fi, fo);
        fclose(fo);
        fclose(fi);
        fi = fopen("out","rb");
        printf("初始归并段各为:\n");
        do{
            k = fread(&b, sizeof(RedType), 1, fi); //读 fi 指针指向的文件,并将读的记录赋值给 b,整个操作成功与否的结果赋值给 k
            if(k == 1){
                if(b.key ==MAXKEY){//当其值等于最大值时,表明当前初始归并段已经完成
                    printf("\n\n");
                    continue;
                }
                print(b);
            }
        }while(k == 1);
        return 0;
    }

    运行结果为:

    文件中的待排序记录为:
    51 49 39 46 38 29 14 61 15 30 1 48 52 3 63 27 4 13 89 24 46 58 33 76
    初始归并段各为:
    29 38 39 46 49 51 61
    
    1 3 14 15 27 30 48 52 63 89
    
    4 13 13 24 33 46 58 76

    6.最佳归并树

    (1)思考

    本节带领大家思考一个问题:

    无论是通过等分还是置换-选择排序得到的归并段

    如何设置它们的归并顺序

    可以使得对外存的访问次数降到最低

    (2)举例参考

    例如,现有通过置换选择排序算法所得到的 9 个初始归并段

    其长度分别为:9,30,12,18,3,17,2,6,24

    在对其采用 3-路平衡归并的方式时可能出现如图 1 所示的情况:

    提示:图 1 中的叶子结点表示初始归并段,各自包含记录的长度用结点的权重来表示;非终端结点表示归并后的临时文件

    假设在进行平衡归并时,操作每个记录都需要单独进行一次对外存的读写

    那么图 1 中的归并过程需要对外存进行读或者写的次数为:

    (9+30+12+18+3+17+2+6+24)*2*2=484(图 1 中涉及到了两次归并,对外存的读和写各进行 2 次)

    从计算结果上看,对于图 1 中的 3 叉树来讲,其操作外存的次数恰好是树的带权路径长度的 2 倍

    对于如何减少访问外存的次数的问题,就等同于考虑如何使 k-路归并所构成的 k 叉树的带权路径长度最短

    若想使树的带权路径长度最短,就是构造赫夫曼树【只要其带权路径长度最短,亦可以称为赫夫曼树

    若对上述 9 个初始归并段构造一棵赫夫曼树作为归并树,如图 2 所示:

    依照图 2 所示,其对外存的读写次数为:

    (2*3+3*3+6*3+9*2+12*2+17*2+18*2+24*2+30)*2=446

    通过以构建赫夫曼树的方式构建归并树,使其对读写外存的次数降至最低

    (k-路平衡归并,需要选取合适的 k 值,构建赫夫曼树作为归并树)

    所以称此归并树为最佳归并树

    7.“虚段”的归并树

    (1)引入

    上述图 2 中所构建的为一颗真正的 3叉树(树中各结点的度不是 3 就是 0)

    而若 9 个初始归并段改为 8 个,在做 3-路平衡归并的时候就需要有一个结点的度为 2

    对于具体设置哪个结点的度为 2,为了使总的带权路径长度最短,正确的选择方法是:附加一个权值为 0 的结点(称为“虚段”)

    例如图 2 中若去掉权值为 30 的结点,其附加虚段的最佳归并树如图 3 所示:

    注意:虚段的设置只是为了方便构建赫夫曼树,在构建完成后虚段自动去掉即可

    (2)如何判断是否需要增加虚段,以及增加多少虚段

    在一般情况下,对于 k–路平衡归并来说
    
    若 (m-1)MOD(k-1)=0,则不需要增加虚段
    
    否则需附加 k-(m-1)MOD(k-1)-1 个虚段

     

    为了提高整个外部排序的效率,本章分别从以上两个方面对外部排序进行了优化

    1. 在实现将初始文件分为 m 个初始归并段时,为了尽量减小 m 的值,采用置换-选择排序算法,可实现将整个初始文件分为数量较少的长度不等的初始归并段
    2. 同时在将初始归并段归并为有序完整文件的过程中,为了尽量减少读写外存的次数,采用构建最佳归并树的方式,对初始归并段进行归并,而归并的具体实现方法是采用败者树的方式

    ==================================================================================

    十三:文件

    https://blog.csdn.net/liu17234050/article/details/104297177

    ==================================================================================

    展开全文
  • 数据结构

    千次阅读 2012-06-28 00:24:43
    1.研究数据结构就是研究( D ) A.数据的逻辑结构 B.数据的存储结构 C.数据的逻辑结构和存储结构 D.数据的逻辑结构、存储结构及其数据在运算上的实现 2.下面关天算法的说法,错误的是( D ) A.为解决某问题的...
  • 数据结构数据结构纠错本

    千次阅读 多人点赞 2018-09-15 11:48:34
    数据结构数据结构纠错本 标签(空格分隔):【考研纠错本】 考研数据结构纠错本 考研数据结构纠错本 1. 数据结构绪论 我的微信公众号 1. 数据结构绪论 对数据...
  • 数据结构答案

    千次阅读 2018-12-22 01:14:27
    数据,数据元素,数据类型,数据结构,逻辑结构,存储结构,算法。 【解答】数据是信息的载体,是描述客观事物的数、字符,以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。 数据元素是数据的基本...
  • 数据结构——树

    万次阅读 多人点赞 2020-05-03 23:28:47
    线性表、栈、队列、串是一对一的数据结构,而树是一对多的数据结构。 树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可...
  • 数据结构面试题

    千次阅读 多人点赞 2018-01-22 10:21:28
    数据结构面试题 1.数据结构与算法常见笔试题    第一章 数据结构与算法 一.算法的基本概念 计算机解题的过程实际上是在实施某种算法,这种算法称为计算机算法。 1.算法的基本特征:可行...
  • 严蔚敏数据结构课后参考答案

    千次阅读 多人点赞 2019-11-09 22:46:44
    数据结构(C语言版)(第2版) 课后习题答案 第1章 绪论 1.简述下列概念:数据、数据元素、数据项、数据对象、数据结构、逻辑结构、存储结构、抽象数据类型。 答案: 数据:是客观事物的符号表示,指所有能输入到...
  • // -----------------------------华丽的分割线---------- /** * Offloaded version of put for null keys */ private V putForNullKey(V value) { for (Entry,V> e = table[0]; e != null; e = e.next) { if ...
  • 2022年考研数据结构_5 树

    万次阅读 2020-12-28 16:40:36
    树5.1 概念5.2 二叉树5.2.1 树的定义和特征深度5.2.2 树的遍历1) 给出中后,输出前序5.3 树,森林5.3.1 树的存储结构5.3.2 森林与二叉树的转换1) 树转换为二叉树2) 森林转换为二叉树3) 二叉树转换为树4) 二叉树转换...
  • 【超详细】数据结构总结及思维导图(王道考研)

    万次阅读 多人点赞 2019-08-21 16:50:23
    数据结构 第一章:数据结构的 基本概念 定义 在任何问题中,数据元素都不是孤立存在的,而是在它们之间存在着某种关系,这种数据元素相互之间的关系称为结构(Structure)。数据结构是相互之间存在一种或多种特定...
  • 数据结构学习整理

    千次阅读 2016-06-06 23:25:00
    数据结构是相互之间存在一种或多种特定关系的数据的集合。 1、抽象层-逻辑结构数据元素之间的逻辑关系称为数据的逻辑结构。数据的逻辑结构可以看作是从具体问题抽象出来的数学模型,它与数据的存储无关。 1.1、集合...
  • 数据结构与算法习题库

    万次阅读 多人点赞 2018-12-26 21:24:55
    1.数据结构被形式地定义为(K,R),其中K是①_B_的有限集合,R是K上的②_D_的有限集合。  ①A.算法 B.数据元素 C.数据操作 D.逻辑结构  ②A.操作 B.映象 C.存储 D.关系 2.算法分析的目的是①C...
  • 数据结构的一些复习点

    万次阅读 多人点赞 2016-06-28 15:41:36
    数据结构知识点总结 概论 1:数据的结构直接影响算法的选择和效率。 2:数据->数据元素(元素,结点,记录)数据的基本单位->数据项(字段,域)数据不可分割的最小单位 3:数据类型:原子数据类型:值不可...
  • 2018山西专升本数据结构知识点总结

    万次阅读 多人点赞 2018-06-29 19:41:36
    2018山西专升本数据结构知识点总结
  • 数据结构名词解释以及简答

    千次阅读 多人点赞 2020-05-20 23:28:52
    数据结构:是相互之间存在一种或多种特定关系的数据元素的集合,是计算机存储和数据组织的方式,它分为三个方面,即数据的逻辑结构,数据的物理结构,数据的操作。 数据项:是数据不可分割的最小单位,用它可以...
  • 【版权申明】未经博主同意,不允许转载!(请尊重原创,博主保留追究权) ... 出自【zejian的博客】 关联文章:java数据结构与算法之顺序表与链表设计与实现...java数据结构与算法之改良顺序表与双链表类似ArrayList和L
  • 数据结构知识点汇总

    万次阅读 多人点赞 2018-07-18 15:44:21
    4、栈通常采用的两种存储结构是(线性存储结构和链表存储结构) 5、队列具有(先进先出)的特征,栈具有(后进先出)的特征。 6、链表(插入和删除不需要移动元素,但是无法随机访问任一元素) 7、循环链表的主要...
  • 数据结构总结及思维导图(王道考研)

    千次阅读 多人点赞 2020-07-27 12:31:52
    数据结构 第一章:数据结构的 基本概念 定义 在任何问题中,数据元素都不是孤立存在的,而是在它们之间存在着某种关系,这种数据元素相互之间的关系称为结构(Structure)。数据结构是相互之间存在一种或多种特定...
  • 数据结构与算法(较全)

    千次阅读 2019-08-11 20:14:32
    数据结构是以某种形式将数据组织在一起的集合,它不仅存储数据,还支持访问和处理数据的操作。 算法是为求解一个问题需要遵循的、被清楚指定的简单指令的集合 线性表 线性表是最常用且最简单的一种数据结构,它是n...
  • 数据结构--原理论述

    千次阅读 2016-09-20 11:41:51
    一、数据结构简介 数据:是描述客观事物的符号,死计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。 数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理,也被...
  • 最全的数据结构归纳总结

    千次阅读 多人点赞 2018-12-17 22:05:58
    数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这些运算后所得到的新结构仍然是原来的结构类型。 数据:所有能被输入到计算机中,且能被计算机...
  • 【算法与数据结构】必备知识点汇总

    万次阅读 多人点赞 2019-07-01 19:27:14
    1.数据结构基础 2.线性表(顺序存储、链式存储) 元素之间是有顺序的:第一个元素无前驱,最后一个元素无后继,其他元素都有前驱和后继 顺序存储结构:用一段地址连续的存储单元一次存储线性表的数据元素(存取...
  • 数据结构与算法常见笔试题

    万次阅读 多人点赞 2017-09-10 11:03:57
    1.数据结构与算法常见笔试题   第一章 数据结构与算法 一.算法的基本概念 计算机解题的过程实际上是在实施某种算法,这种算法称为计算机算法。 1.算法的基本特征:可行性,确定性,有穷性,拥有足够的情报。 2....

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 25,308
精华内容 10,123
关键字:

线形态数据结构

数据结构 订阅