bytestoread有数据时也为0_数据结构(邓俊辉/严蔚敏)的学习

本文详细介绍了邓俊辉/严蔚敏《数据结构》中的核心概念,包括向量、链表、栈与队列、二叉树的遍历和性质、图的最小生成树以及查找算法如二分查找。文中还探讨了C语言实现的数据结构,并提到了如何实现二叉树的序列化与反序列化。
摘要由CSDN通过智能技术生成

本文使用 Zhihu On VSCode 创作并发布

  • 数据结构(邓俊辉/严蔚敏)的学习
    • 第一章 绪论
    • 第二章 向量
    • 第三章 列表
      • C语言实现链表
      • 内核链表/通用链表
    • 第四章 栈与队列
      • 波兰式转逆波兰式
      • 使用顺序表实现循环队列
      • N皇后
    • 第五章 二叉树
      • 先中后序遍历的非递归写法
      • 二叉树性质(根节点层数为1)
      • 由n个节点可以构造出多少种不同的二叉树?
      • 满二叉树(Full Binary Tree / Perfect Binary Tree)
      • 完全二叉树(Complete Binary Tree)
      • 二叉树的重构(要求遍历序列无重复数字)
      • Huffman编码树的简单实现,对文本进行编码与解码
      • 中序线索二叉树(Threaded Binary Tree)的简单实现
      • 树和森林
      • 其他
      • 最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化
    • 第六章 图
      • 最小生成树(本质都是贪心)
    • 第七章 查找 (严蔚敏版)
      • 二分(折半)查找
      • 分块查找
      • 二叉搜索树
      • 平衡二叉树

可能更好的阅读体验与更快的更新

第一章 绪论

  1. n二进制展开的位数 , 同理
    为十进制 数
    n 的位数.
  2. 实际上,由基本的数论知识不难验证 , 任意整数的平方关于5整除之后的余数,断乎不可能是2(或3).

    • ,
      ,故
  3. 经过条件判断 (12 < n) && (sqrt(f) * sqrt(f) == f)之后的“递归调用”,依然绝对不可能被执行 . 实际上在Fibonacci数中,只有fib(0)、fib(1)、fib(2)、fib(12) 是平方数,fib(n > 12)必然都不是。

    • 对于整数或者浮点数 f ,只有 f 为平方数时 ,sqrt(f) * sqrt(f) == f 才为真
  4. 因为这篇文章,简单学了latex,写出来的公式真漂亮.

第二章 向量

  1. 本书应该是默认使用了#include <utility>; using namespace std; ,因为代码中使用了swap;同时 rand()包含在头文件<cstdlib>

  2. Visual studio code对c++的模板没有语法检查 原因 , 对于使用了模板的C++代码推荐使用Visual Studio ,在指定了模板的类型后有语法检查.

  3. p37使用的置乱算法是高纳德置乱算法(Knuth-Fisher-Yates algorithm)

    • 我想到的是Naive algorithm

    ```
    for(int i = 0 ;i < n ;i++)
    {
    	swap(a[i],a[rand() % n]);
    }
    ```
    
    • 这种写法得到的所有可能结果有

      种,而不是 $ n! $ 种,而且
      不可能是
      的整数倍。Reference

      比如说 arr = {1,2,3},正确的结果应该有

      种可能,而这种写法总共有
      种可能结果。因为 27 不能被 6 整除,所以一定有某些情况被「偏袒」了,也就是说某些情况出现的概率会大一些,所以这种打乱结果不算「真的乱」。
  4. 二分搜索 根据 std::lower_bound() 去实现 , 这个 回答 我认为很好的解释了思想.

  5. 遍历操作Vector<T>::traverse() 使用了函数对象 . 我的理解是 函数对象就是重载了operator()的类

  6. 书中binSearch() , fibSearch()声明为static 函数,即为全局静态函数,其意义在于 作用域局限于一个源文件内,静态函数不能被其它文件所用.

  7. Vector代码实现

  8. C语言实现线性表

第三章 列表

  1. C++11标准之后,可以使用template <typename T> using ListNodePosi = ListNode<T>*; 代替书中的#define ListNodePosi(T) ListNode<T>* , 但是请记得前置声明类.

  2. 在本书列表的实现中,我发现了一些错误 我购买书的印次是2019年10月第17次印刷,书中我所说部分的代码是错误的,邓老师主页的代码是正确的

    1. 将临时变量传递给了非const引用 ,函数first()返回的是值为header->succ临时变量,在重载的merge(...)作为了类型为ListNodePosi(T)&的参数

      #defind  ListNodePosi(T) ListNode<T> *
      ListNodePosi(T) first() const { return header->succ; }
      void merge(ListNodePosi(T)& ,int ,List<T>& ,ListNodePosi(T) ,int){ //实现省略... }
      void merge(List<T>& L)
      {
      	merge(first(),size,L,L.first(),L._size);
      }
      
    2. 第二个其实也是类型的错误,很明显了

      T& List<T>::operator[] (Rank r) const
      {
      	//...
      }
      void copyNodes(ListNodePosi(T) p,int n){ // ...  }
      List<T>::List(List<T> const& L,int r,int n)
      {
          copyNodes(L[r],n);
      }
      
  3. 双向链表代码实现

C语言实现链表

  1. 简单的双向链表实现,同时也实现了归并排序.
  2. 代码实现
  3. 今天在用C语言写链表的归并排序时,在同一个链表中merge其中两个有序序列时,传进去的指针所指向的地方可能会被free掉,从而影响后续的归并.需要保存前一个节点,然后通过pred->succ找到它自己. 看懂了代码,理解了思想,不等于会写了.所有东西你都要自己实现一遍,我觉得很好的例子就是二分搜索,思想很简单,但你要bug free实现一个,还是需要自己去打磨熟悉,这一过程我感觉比你去理解他更费功夫. 数据结构的学习,我使用邓老师这个教材去学,但每一部分我都要尽量用C语言去实现一遍.

内核链表/通用链表

  1. Linux内核的实现,大家可以根据Github@Akagi201的移植,去学习 Port linux kernel list.h to userspace.
  2. Linux kernel list 的讲解 .
  3. 自己的实现 与 测试 ,仅实现了双向链表部分.
  4. 写法较传统链表没有太多区别,但思想很精妙,尤其是container_ofoffsetof非常好得体现了C语言对内存的操作.
  5. 关于offsetof
    1. 解空指针被大多标准明确定义为未定义行为,但对于编译器,它可以define the behavior. 在GNU中,typeid就依赖于解空指针,可见解空指针不一定是UB. 更多

    2. 特别的,在C++中

      1. offsetof不能以标准 C++ 实现,并要求编译器支持: GCC 、 LLVM

        offsetof (type, member) :若 type 不是简旧数据类型 (PODType) (C++11 前)标准布局类型 (C++11 起),则行为未定义 (C++17 前)

        C++17 : Use of the offsetof macro with a type other than a standard-layout class is conditionally-supported.

      2. POD类型 和 标准布局类型

    3. offsetof宏自己实现仅作学习用途 , 因此#include <stddef.h> 依靠编译器实现才是正道.

第四章 栈与队列

  1. 栈的实现

  2. 队列的实现

波兰式转逆波兰式

  1. Code
        //b优先级是否低于a
        bool isPriority(char a, char b)
        {
            if ((a == '-' or a == '+') and (b == '*' or b == '/'))
                return false;
            return true;
        }
    
        /*
            样例: infixToPostfix("96 + ( 5 * 6 - 3 ! ) + 10 / 2") == "96 5 6 * 3 ! - + 10 2 / + "
        */
        string infixToPostfix(const string& source)
        {
            string res;
            stack<char> s;
            for (int i = 0; i < source.size(); i++)
            {
                switch (source[i])
                {
                case ' ':
                    break;
                case '(':
                    s.push('(');
                    break;
                case ')':
                    while (s.top() != '(')
                    {
                        char op = s.top();
                        res.push_back(op);
                        res.push_back(' ');
                        s.pop();
                    }
                    s.pop();
                    break;
                case '!':
                    res.push_back('!'), res.push_back(' ');
                    break;
                default:
                    if (isdigit(source[i]))
                    {
                        res.push_back(source[i]);
                        if (i + 1 > source.size() - 1 or !isdigit(source[i + 1]))
                            res.push_back(' ');
                    }
                    else
                    {
                        while (!s.empty() and s.top() != '(' and isPriority(s.top(), source[i]))
                        {
                            char op = s.top();
                            s.pop();
                            res.push_back(op);
                            res.push_back(' ');
                        }
                        s.push(source[i]);
                    }
                }
            }
            while (!s.empty())
            {
                res.push_back(s.top());
                res.push_back(' ');
                s.pop();
            }
            return res;
        }
    

使用顺序表实现循环队列

  1. 判断队空/队满
    1. 浪费一个元素的存储空间:front指向队首的实际位置,rear指向实际位置的下一个位置,front==rear时为空,(rear+1) % m == front时为满
    2. 使用标记tag,front指向队首实际位置,rear指向队尾实际位置的下一个位置,入队时tag=1,出队时tag=0,当front == rear && tag == 0时队空,当front == rear && tag == 1时队满
    3. 使用front,rear, length作为判断队空队满的标志,这样就不用浪费一个元素的存储空间了,和tag的作用是一样的,front指向队首实际位置,rear指向队尾实际位置 , length == MAXSIZE队满
  2. 队列大小 (rear - head + MAXSIZE) % MAXSIZE

N皇后

  1. Code

        // https://vjudge.net/problem/HDU-2553
        #include <bits/stdc++.h>
        using namespace std;
        int n;
    	int column[11];
        int cnt = 0;
        void dfs(int row)
        {
            if (row > n)
            {
                cnt++;
                return;
            }
            for (int i = 1; i <= n; i++)
            {
                bool flag = true;
                for (int j = 1; j < row; j++)
                {
                    if (i == column[j] || column[j] - j == i - row || column[j] + j == i + row)
                    {
                        flag = false;
                        break;
                    }
                }
                if (flag)
                {
                    column[row] = i;
                    dfs(row + 1);
                }
            }
        }
        int main(void)
        {
            while (cin >> n && n != 0)
            {
                memset(column, 0, sizeof(column));
                cnt = 0;
                dfs(1);
                cout << cnt << endl;
            }
            return 0;
        }
    

第五章 二叉树

先中后序遍历的非递归写法

  1. 先中后序遍历的非递归写法(称之为迭代法可能不太严谨)

    1. 书中遍历的迭代写法中除了中序遍历利用节点的直接后继达到了O(1)的空间复杂度,其余均需要利用辅助栈,因此我个人认为在使用了栈的前提下,老师书中的写法理解起来并没有那么轻松,反倒是利用空指针作为根节点的标记的写法理解起来容易,整体想法与递归将近.

二叉树性质(根节点层数为1)

  1. 二叉树性质
    1. 二叉树第
      层上至多有
      个节点.
    2. 深度为
      的二叉树至多有
      个节点,至少有
      个节点 .
    3. 对于任何一棵二叉树,若其叶子节点数为
      ,度为
      的节点数为
      ,则
    4. 包含
      个节点的二叉树,其高度至少为
      ,即完全二叉树,至多为$n $,此时为一棵斜树.

由n个节点可以构造出多少种不同的二叉树?

满二叉树(Full Binary Tree / Perfect Binary Tree)

  1. 满二叉树
    • 所有internal node都有兩個subtree
    • 所有leaf node具有相同的level(或相同的height)
    • 按照层序遍历从上到下,从左到右编号,

完全二叉树(Complete Binary Tree)

  1. 完全二叉树
    • 对于具有n个结点的二叉树按层序编号,如果每个结点的编号与同样深度的满二叉树中对应编号的结点在二叉树中位置完全相同,则该二叉树称为完全二叉树。
    • 编号规律与满二叉树一致
    • 具有
      个节点的完全二叉树的高度为
    • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
    • 叶子结点只出现在最下两层,最下层的叶子一定集中在左部连续位置,倒数二层若有叶子节点一定集中在右部连续位置
    • 如果结点度为1,则该结点只有左孩子

二叉树的重构(要求遍历序列无重复数字)

  1. 对于任何二叉树,知道其[先序|后续]与中序可唯一确定一棵二叉树

  2. 对于真二叉树,知道其先序与后序可唯一确定一棵真二叉树(真二叉树是各个节点的子树个数均为偶数(

    )的二叉树)
  3. 普通二叉树与真二叉树重构 代码实现

Huffman编码树的简单实现,对文本进行编码与解码

  1. Huffman编码树的简单实现,对文本进行编码与解码

    1. 正确性验证

      856b3b6a62b31795168332f1809b66fe.png
      Image

中序线索二叉树(Threaded Binary Tree)的简单实现

  1. 中序线索二叉树(Threaded Binary Tree)的简单实现

    1. #图解 数据结构:轻松搞定线索二叉树 - 知乎 (zhihu.com)
    2. 线索二叉树是将每个节点的空指针域(即左/右孩子为空)利用起来,将原本为空的指针指向其在某种遍历序列中的直接前继或者直接后继,用L/RTag去区别指针指向的是孩子还是其前继/后继.
    3. 在对二叉树中序线索化的函数inThreading()
      1. 基本逻辑就是递归版的中序遍历,在遇到当前节点的lchild为空时,修改lchild指针为当前节点的直接前继,更改LTagtrue. (当前节点的直接前继用全局变量pre指针表示,初始为NULL,在遍历过程中对其进行修改.pre始终为刚刚访问完的节点) .如果pre的右孩子为空,修改prerchild指向他的直接后继,也就是当前节点.
      2. 因为中序遍历的默认顺序是左根右,因此对于当前节点其前继是已知的即pre,所以我们修改他的lchild;对于pre节点,其后继是已知的,所以我们修改pre->rchild.
      3. 在完成中序遍历线索化之后,我们可以知道对于整个二叉树,只有中序序列的第一个节点的lchild与最后一个节点的rchild指针为空.当L/RTag == true时,该节点无左/右孩子.
    4. 在对中序线索二叉树的遍历函数inOrderThread()
      1. 首先通过firstAtInorder()函数找到该二叉树中序序列的第一个节点.第一个节点为该树最左侧通路的最深的那个节点(邓俊辉 DSA p128),也就是树最左侧那个节点. 只要沿着节点,一路走左孩子,直到LTag == true即可.
      2. 接着找当前节点的后继节点nextAtInorder().如果当前节点没有右孩子即RTag == true,则其后继为lchild;如果有,则为当前节点的右子树中的最左侧的节点,使用firstAtInorder()即可.
      3. 对于左根右的中序遍历,更改空的lchild指向他的直接前继并没有什么用.
      4. 强烈建议这部分和邓老师的迭代版遍历那部分一起看,邓老师中序遍历空间O(1)的写法也提到直接后继,书上的图也帮助理解.
    5. 我讨厌线索二叉树

树和森林

  1. 树和森林
    1. 树的存储方式

      • 双亲表示法 : 用一组连续空间储存pair<data,parent's index>. 双亲这翻译太误导了
      • 孩子表示法 : 孩子链表 / 带双亲的孩子链表
      • 孩子兄弟表示法 : 又称二叉树表示法/二叉链表表示法,指针指向第一个孩子节点与下一个兄弟节点. 当说到二叉链表时,我也不知道是指孩子孩子还是孩子兄弟 . 多叉树可等价为二叉树就是用这种表示方法.
    2. 森林与二叉树转换

      1. 由孩子兄弟二叉链表表示法我们知道任何一颗树的根节点的右子树为空,我们可以认为森林中第二棵树的根节点是第一棵树的根节点的兄弟.

其他

  1. 一棵二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树为高度等于其结点个数的二叉树,即任意结点只有左孩子或只有右孩子,先序序列即从上向下的层序,后序序列即从下向上的层序.

  2. 二叉树的储存结构

    1. 顺序存储结构
      • 对于完全二叉树,将编号为i的节点一维数组中下标为i-1的位置
      • 对于一般二叉树,将其节点与完全二叉树上的节点对应. 最坏情况下,有n个节点的右斜树,需要
        的空间.
    2. 链式存储结构
      • 二叉链表
      • 三叉链表 ...

最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化

  1. 最后,如何去储存一棵二叉树呢? LeetCode297. 二叉树的序列化与反序列化
    • 层序遍历,序列中 每个有效节点后面的第二/三个节点分别代表其左右孩子.

    • /*
      struct TreeNode
      {
          int val;
          TreeNode* left;
          TreeNode* right;
          TreeNode(int x)
              : val(x), left(NULL), right(NULL)
          {
          }
      };
      */
      class Codec
      {
        public:
          // Encodes a tree to a single string.
          string serialize(TreeNode* root)
          {
              queue<TreeNode*> q;
              string res;
              q.push(root);
              while (not q.empty())
              {
                  TreeNode* now = q.front();
                  q.pop();
                  if (now)
                  {
                      q.push(now->left);
                      q.push(now->right);
                      res += to_string(now->val);
                      res.push_back(',');
                  }
                  else
                      res += "null,";
              }
              return res;
          }
          // Decodes your encoded data to tree.
          TreeNode* deserialize(string data)
          {
              size_t l = 0, r = data.find(',');
              string sub = data.substr(l, r - l);
              if (sub == "null")
                  return nullptr;
              int num        = stoi(sub);
              TreeNode* root = new TreeNode(num);
              queue<TreeNode*> q;
              q.push(root);
              while (not q.empty())
              {
                  TreeNode* now = q.front();
                  q.pop();
                  l   = r + 1;
                  r   = data.find(',', l);
                  sub = data.substr(l, r - l);
                  if (sub != "null")
                  {
                      num       = stoi(sub);
                      now->left = new TreeNode(num);
                      q.push(now->left);
                  }
                  l   = r + 1;
                  r   = data.find(',', l);
                  sub = data.substr(l, r - l);
                  if (sub != "null")
                  {
                      num        = stoi(sub);
                      now->right = new TreeNode(num);
                      q.push(now->right);
                  }
              }
              return root;
          }
      };
      

第六章 图

  1. 强连通图 : 在有向图G中,如果两个顶点间至少存在一条互相可达路径,称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。
  2. 强连通分量 : 非强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。
  3. 十字链表 与 邻接多重表
  4. 深度优先生成树 与 广度优先生成树

最小生成树(本质都是贪心)

  1. 最小生成树
    1. Prim算法
      • 基于邻接矩阵的图 时间复杂度
      • 因为时间复杂度与
        无关,适用于求稠密网的最小生成树
      • 基于邻接表的图 + 二叉堆优化 时间复杂度
    2. Kruskal算法 使用堆排序时间复杂度
      • 更适用于求稀疏网的最小生成树
  2. 最短路
    1. Dijkstra
    2. Floyd
  3. 关键路径 这东西不就是个最长路嘛?

第七章 查找 (严蔚敏版)

  1. 顺序查找

二分(折半)查找

  1. 二分搜索
    1. lower_bound,upper_bound,binarySearch代码实现 , 可参照CppReference中的实现
      • 二分想着很简单,但要是Bug Free的实现没那么简单lower/upper_bound这俩兄弟容易把人写懵 最开始end指向数组尾的下一个元素,为了在没找到tar时返回数组尾下一个位置.当*mid元素非法时,修改begin.反之,修改end.直到beginend重合,即为目标位置. 这里的非法指我们找的元素一定不在mid之前,我们就可以把mid之前的元素直接排除掉,令begin= mid + 1.当*mid元素合法时,即符合我们的搜索条件,因为我们要找到时第一个符合条件位置,因此mid之后的元素都不用考虑了,故修改end = mid. 记住三点, 1. 最开始end指向数组尾下一个元素 2. 始终将target控制在[begin,end]之内 3.我们要找的的第一个符合条件的元素.以此去理解end的变化
      • binary_search实现只需要调用lower_bound,判断返回指针是否是合法地址&&指向元素是否等于target即可.

分块查找

  1. 分块查找
    1. 要求表本身分块有序,即下一个子表中的最小值大于当前子表最大值.
    2. 由此建立索引表,要对索引表进行排序.

二叉搜索树

  1. 二叉搜索树
    1. https://www.deltablog.xyz/posts/binarysearchtree/
    2. 最坏情况下与顺序查找相同.
    3. 节点的删除
      1. 左子树上的值小于根节点,右子树的值大于根节点
      2. 当删除节点左右子树都存在时,找其左子树中最大的节点或者右子树中最小的节点代替当前节点即可.
      3. 代码实现 ... 写的一点也不好,罗里吧嗦...但至少能跑了,没发现bug

平衡二叉树

  1. Something
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值