重学数据结构五:数据结构基础-树和二叉树、哈希表

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 重学数据结构五:数据结构基础-树和二叉树、哈希表

24/100

发布文章

网络异常,图片无法展示
|

加粗

斜体

标题

删除线

无序

有序

待办

引用

代码块

图片

视频

表格

超链接

投票

导入

导出

保存

撤销

重做

模版

使用富文本编辑器

目录

发文助手

语法说明

##

树是由结点和边组成的,不存在环的一种数据结构。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711234953664.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTY5NjQz,size_16,color_FFFFFF,t_70)
树满足递归定义的特性。也就是说,如果一个数据结构是树结构,那么剔除掉根结点后,得到的若干个子结构也是树,通常称作子树。

1)A 结点是 B 结点和 C 结点的上级,则 A 就是 B 和 C 的父结点,B 和 C 是 A 的子结点。
2)B 和 C 同时是 A 的“孩子”,则可以称 B 和 C 互为兄弟结点。
3)A 没有父结点,则可以称 A 为根结点。
4)G、H、I、F 结点都没有子结点,则称 G、H、I、F 为叶子结点。

## 二叉树

1)满二叉树,定义为除了叶子结点外,所有结点都有 2 个子结点。
2)完全二叉树,定义为除了最后一层以外,其他层的结点个数都达到最大,并且最后一层的叶子结点都靠左排列。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235029375.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTY5NjQz,size_16,color_FFFFFF,t_70)
存储二叉树有两种办法,一种是基于指针的链式存储法,另一种是基于数组的顺序存储法。
1)链式存储法,也就是像链表一样,每个结点有三个字段,一个存储数据,另外两个分别存放指向左右子结点的指针,如下图所示:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235058221.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTY5NjQz,size_16,color_FFFFFF,t_70)
2)顺序存储法,就是按照规律把结点存放在数组里,如下图所示,为了方便计算,我们会约定把根结点放在下标为 1 的位置。随后,B 结点存放在下标为 2 的位置,C 结点存放在下标为 3 的位置,依次类推。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235126628.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTY5NjQz,size_16,color_FFFFFF,t_70)
根据这种存储方法,我们可以发现如果结点 X 的下标为 i,那么 X 的左子结点总是存放在 2 * i 的位置,X 的右子结点总是存放在 2 * i + 1 的位置。
之所以称为完全二叉树,是从存储空间利用效率的视角来看的。对于一棵完全二叉树而言,仅仅浪费了下标为 0 的存储位置。而如果是一棵非完全二叉树,则会浪费大量的存储空间。

**树的基本操作**
1)前序遍历,对树中的任意结点来说,先打印这个结点,然后前序遍历它的左子树,最后前序遍历它的右子树。
2)中序遍历,对树中的任意结点来说,先中序遍历它的左子树,然后打印这个结点,最后中序遍历它的右子树。
3)后序遍历,对树中的任意结点来说,先后序遍历它的左子树,然后后序遍历它的右子树,最后打印它本身。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235152135.png)

```java
// 先序遍历
publicstaticvoidpreOrderTraverse(Node node){
   if(node ==null)
       return;
   System.out.print(node.data +" ");
   preOrderTraverse(node.left);
   preOrderTraverse(node.right);
}
// 中序遍历
publicstaticvoidinOrderTraverse(Node node){
   if(node ==null)
       return;
   inOrderTraverse(node.left);
   System.out.print(node.data +" ");
   inOrderTraverse(node.right);
}
// 后序遍历
publicstaticvoidpostOrderTraverse(Node node){
   if(node ==null)
       return;
   postOrderTraverse(node.left);
   postOrderTraverse(node.right);
   System.out.print(node.data +" ");
}
```

二叉树遍历过程中,每个结点都被访问了一次,其时间复杂度是 O(n)。
执行增加和删除数据的操作时,我们只需要通过指针建立连接关系就可以了。
对于没有任何特殊性质的二叉树而言,抛开遍历的时间复杂度以外,真正执行增加和删除操作的时间复杂度是 O(1)。树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)。

**二叉查找树的特性**
二叉查找树(也称作二叉搜索树)具备以下几个的特性:
1)在二叉查找树中的任意一个结点,其左子树中的每个结点的值,都要小于这个结点的值。
2)在二叉查找树中的任意一个结点,其右子树中每个结点的值,都要大于这个结点的值。
3)在二叉查找树中,会尽可能规避两个结点数值相等的情况。
**二叉查找树的查找操作**
在利用二叉查找树执行查找操作时,我们可以进行以下判断:
1)首先判断根结点是否等于要查找的数据,如果是就返回。
2)如果根结点大于要查找的数据,就在左子树中递归执行查找动作,直到叶子结点。
3)如果根结点小于要查找的数据,就在右子树中递归执行查找动作,直到叶子结点。
这样的“二分查找”所消耗的时间复杂度就可以降低为 O(logn)。
**二叉查找树的插入操作**
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235308637.gif)
二叉查找树插入数据的时间复杂度是 O(logn)。但这并不意味着它比普通二叉树要复杂。原因在于这里的时间复杂度更多是消耗在了遍历数据去找到查找位置上,真正执行插入动作的时间复杂度仍然是 O(1)。
**二叉查找树的删除操作**
情况一,如果要删除的结点是某个叶子结点,则直接删除,将其父结点指针指向 null 即可。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235358544.gif)
情况二,如果要删除的结点只有一个子结点,只需要将其父结点指向的子结点的指针换成其子结点的指针即可。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235427928.gif)
情况三,如果要删除的结点有两个子结点,则有两种可行的操作方式。
第一种,找到这个结点的左子树中最大的结点,替换要删除的结点。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235454561.gif)
第二种,找到这个结点的右子树中最小的结点,替换要删除的结点。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235519729.gif)
**树的案例**
例1:
输入一个字符串,判断它在已有的字符串集合中是否出现过?(假设集合中没有某个字符串与另一个字符串拥有共同前缀且完全包含的特殊情况,例如 deep 和 dee。)如,已有字符串集合包含 6 个字符串分别为,cat, car, city, dog,door, deep。输入 cat,输出 true;输入 home,输出 false。

我们假设采用最暴力的办法,估算一下时间复杂度。假设字符串集合包含了 n 个字符串,其中的字符串平均长度为 m。那么新来的一个字符串,需要与每个字符串的每个字符进行匹配。则时间复杂度为 O(nm)。
但在 nm 的复杂度中,显然存在很多的无效匹配。例如,输入 home 时,6 个字符串都没有 h 开头的,则不需要进行后续的匹配。因此,如果可以通过对字符前缀进行处理,就可以最大限度地减少无谓的字符串比较,从而提高查询效率。这就是“用空间换时间”的思想,再利用共同前缀来提高查询效率。
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235620251.gif)
例2:
给定一棵树,按照层次顺序遍历并打印这棵树。例如:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235716674.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTY5NjQz,size_16,color_FFFFFF,t_70)

```java
publicstaticvoidlevelTraverse(Node root){
   if(root ==null){
       return;
   }

   LinkedList<Node> queue =newLinkedList<Node>();
   Node current =null;
   queue.offer(root);// 根节点入队

   while(!queue.isEmpty()){// 只要队列中有元素,就可以一直执行,非常巧妙地利用了队列的特性
       current = queue.poll();// 出队队头元素
       System.out.print("-->"+ current.data);
       // 左子树不为空,入队
       if(current.leftChild !=null)
           queue.offer(current.leftChild);

       // 右子树不为空,入队
       if(current.rightChild !=null)
           queue.offer(current.rightChild);
   }
}

```

## 哈希表

哈希表名字源于 Hash,也可以叫作散列表。哈希表是一种特殊的数据结构,它与数组、链表以及树等我们之前学过的数据结构相比,有很明显的区别。
1)线性表中的栈和队列对增删有严格要求,它们会更关注数据的顺序。
2)数组和字符串需要保持数据类型的统一,并且在基于索引的查找上会更有优势。
3)树的优势则体现在数据的层次结构上。
但它们普遍都存在这样的缺陷,那就是数据数值条件的查找,都需要对全部数据或者部分数据进行遍历。
hash表的增删改查时间复杂度都是o(1)
**哈希表的核心思想**
哈希表的设计采用了函数映射的思想,将记录的存储位置与记录的关键字关联起来。这样的设计方式,能够快速定位到想要查找的记录,而且不需要与表中存在的记录的关键字比较后再来进行查找。

如果有一种方法,可以实现“地址 = f (关键字)”的映射关系,那么就可以快速完成基于数据的数值的查找了。这就是哈希表的核心思想。

**如何设计哈希函数**
第一,直接定制法
哈希函数为关键字到地址的线性函数。如,H (key) = a*key + b。 这里,a 和 b 是设置好的常数。
第二,数字分析法
假设关键字集合中的每个关键字 key 都是由 s 位数字组成(k1,k2,…,Ks),并从中提取分布均匀的若干位组成哈希地址。上面张一、张二、张三、张四的手机号信息存储,就是使用的这种方法。
第三,平方取中法
如果关键字的每一位都有某些数字重复出现,并且频率很高,我们就可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址。
第四,折叠法
如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址。
第五,除留余数法
预先设置一个数 p,然后对关键字进行取余运算。即地址为 key mod p。
**如何解决哈希冲突**
第一,开放定址法

即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在哈希表中形成一个探测序列,然后沿着这个探测序列依次查找下去。当碰到一个空的单元时,则插入其中。
常用的探测方法是线性探测法:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235843430.gif)
第二,链地址法
将哈希地址相同的记录存储在一张线性链表中。
例如,有一组关键字 {12,13,25,23,38,84,6,91,34},采用的哈希函数为 key mod 11。如下图所示:
![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20200711235913596.gif)
**哈希表的基本操作**
在很多高级语言中,哈希函数、哈希冲突都已经在底层完成了黑盒化处理,是不需要开发者自己设计的。也就是说,哈希表完成了关键字到地址的映射,可以在常数级时间复杂度内通过关键字查找到数据。
实现细节,比如用了哪个哈希函数,用了什么冲突处理,甚至某个数据记录的哈希地址是多少,都是不需要开发者关注的。

**哈希表的案例**
假设有一个在线系统,可以实时接收用户提交的字符串型关键字,并实时返回给用户累积至今这个关键字被提交的次数。
例如,用户输入"abc",系统返回 1。用户再输入"jk",系统返回 1。用户再输入"xyz",系统返回 1。用户再输入"abc",系统返回 2。用户再输入"abc",系统返回 3。

**一种解决方法是**,用一个数组保存用户提交过的所有关键字。当接收到一个新的关键字后,插入到数组中,并且统计这个关键字出现的次数。
根据数组的知识可以计算出,插入到最后的动作,时间复杂度是 O(1)。但统计出现次数必须要全部数据遍历一遍,时间复杂度是 O(n)。随着数据越来越多,这个在线系统的处理时间将会越来越长。显然,这不是一个好的方法。

**如果采用哈希表**,则可以利用哈希表新增、查找的常数级时间复杂度,在 O(1) 时间复杂度内完成响应。预先定义好哈希表后(可以采用 Map < String, Integer > d = new HashMap <> (); )对于关键字(用变量 key_str 保存),判断 d 中是否存在 key_str 的记录。
如果存在,则把它对应的value(用来记录出现的频次)加 1;
如果不存在,则把它添加到 d 中,对应的 value 赋值为 1。最后,打印处 key_str 对应的 value,即累积出现的频次。

```java
if(d.containsKey(key_str){
   d.put(key_str, d.get(key_str)+1);
}
else{
   d.put(key_str,1);
}
System.out.println(d.get(key_str));

```

树是由结点和边组成的,不存在环的一种数据结构。

树满足递归定义的特性。也就是说,如果一个数据结构是树结构,那么剔除掉根结点后,得到的若干个子结构也是树,通常称作子树。

1)A 结点是 B 结点和 C 结点的上级,则 A 就是 B 和 C 的父结点,B 和 C 是 A 的子结点。

2)B 和 C 同时是 A 的“孩子”,则可以称 B 和 C 互为兄弟结点。

3)A 没有父结点,则可以称 A 为根结点。

4)G、H、I、F 结点都没有子结点,则称 G、H、I、F 为叶子结点。

二叉树

1)满二叉树,定义为除了叶子结点外,所有结点都有 2 个子结点。

2)完全二叉树,定义为除了最后一层以外,其他层的结点个数都达到最大,并且最后一层的叶子结点都靠左排列。

存储二叉树有两种办法,一种是基于指针的链式存储法,另一种是基于数组的顺序存储法。

1)链式存储法,也就是像链表一样,每个结点有三个字段,一个存储数据,另外两个分别存放指向左右子结点的指针,如下图所示:

2)顺序存储法,就是按照规律把结点存放在数组里,如下图所示,为了方便计算,我们会约定把根结点放在下标为 1 的位置。随后,B 结点存放在下标为 2 的位置,C 结点存放在下标为 3 的位置,依次类推。

根据这种存储方法,我们可以发现如果结点 X 的下标为 i,那么 X 的左子结点总是存放在 2 * i 的位置,X 的右子结点总是存放在 2 * i + 1 的位置。

之所以称为完全二叉树,是从存储空间利用效率的视角来看的。对于一棵完全二叉树而言,仅仅浪费了下标为 0 的存储位置。而如果是一棵非完全二叉树,则会浪费大量的存储空间。

树的基本操作

1)前序遍历,对树中的任意结点来说,先打印这个结点,然后前序遍历它的左子树,最后前序遍历它的右子树。

2)中序遍历,对树中的任意结点来说,先中序遍历它的左子树,然后打印这个结点,最后中序遍历它的右子树。

3)后序遍历,对树中的任意结点来说,先后序遍历它的左子树,然后后序遍历它的右子树,最后打印它本身。

// 先序遍历
public static void preOrderTraverse(Node node) {
    if (node == null)
        return;
    System.out.print(node.data + " ");
    preOrderTraverse(node.left);
    preOrderTraverse(node.right);
}
// 中序遍历
public static void inOrderTraverse(Node node) {
    if (node == null)
        return;
    inOrderTraverse(node.left);
    System.out.print(node.data + " ");
    inOrderTraverse(node.right);
}
// 后序遍历
public static void postOrderTraverse(Node node) {
    if (node == null)
        return;
    postOrderTraverse(node.left);
    postOrderTraverse(node.right);
    System.out.print(node.data + " ");
}

二叉树遍历过程中,每个结点都被访问了一次,其时间复杂度是 O(n)。

执行增加和删除数据的操作时,我们只需要通过指针建立连接关系就可以了。

对于没有任何特殊性质的二叉树而言,抛开遍历的时间复杂度以外,真正执行增加和删除操作的时间复杂度是 O(1)。树数据的查找操作和链表一样,都需要遍历每一个数据去判断,所以时间复杂度是 O(n)。

二叉查找树的特性

二叉查找树(也称作二叉搜索树)具备以下几个的特性:

1)在二叉查找树中的任意一个结点,其左子树中的每个结点的值,都要小于这个结点的值。

2)在二叉查找树中的任意一个结点,其右子树中每个结点的值,都要大于这个结点的值。

3)在二叉查找树中,会尽可能规避两个结点数值相等的情况。

二叉查找树的查找操作

在利用二叉查找树执行查找操作时,我们可以进行以下判断:

1)首先判断根结点是否等于要查找的数据,如果是就返回。

2)如果根结点大于要查找的数据,就在左子树中递归执行查找动作,直到叶子结点。

3)如果根结点小于要查找的数据,就在右子树中递归执行查找动作,直到叶子结点。

这样的“二分查找”所消耗的时间复杂度就可以降低为 O(logn)。

二叉查找树的插入操作

二叉查找树插入数据的时间复杂度是 O(logn)。但这并不意味着它比普通二叉树要复杂。原因在于这里的时间复杂度更多是消耗在了遍历数据去找到查找位置上,真正执行插入动作的时间复杂度仍然是 O(1)。

二叉查找树的删除操作

情况一,如果要删除的结点是某个叶子结点,则直接删除,将其父结点指针指向 null 即可。

情况二,如果要删除的结点只有一个子结点,只需要将其父结点指向的子结点的指针换成其子结点的指针即可。

情况三,如果要删除的结点有两个子结点,则有两种可行的操作方式。

第一种,找到这个结点的左子树中最大的结点,替换要删除的结点。

第二种,找到这个结点的右子树中最小的结点,替换要删除的结点。

树的案例

例1:

输入一个字符串,判断它在已有的字符串集合中是否出现过?(假设集合中没有某个字符串与另一个字符串拥有共同前缀且完全包含的特殊情况,例如 deep 和 dee。)如,已有字符串集合包含 6 个字符串分别为,cat, car, city, dog,door, deep。输入 cat,输出 true;输入 home,输出 false。

我们假设采用最暴力的办法,估算一下时间复杂度。假设字符串集合包含了 n 个字符串,其中的字符串平均长度为 m。那么新来的一个字符串,需要与每个字符串的每个字符进行匹配。则时间复杂度为 O(nm)。

但在 nm 的复杂度中,显然存在很多的无效匹配。例如,输入 home 时,6 个字符串都没有 h 开头的,则不需要进行后续的匹配。因此,如果可以通过对字符前缀进行处理,就可以最大限度地减少无谓的字符串比较,从而提高查询效率。这就是“用空间换时间”的思想,再利用共同前缀来提高查询效率。

例2:

给定一棵树,按照层次顺序遍历并打印这棵树。例如:

public static void levelTraverse(Node root) {
    if (root == null) {
        return;
    }
    LinkedList<Node> queue = new LinkedList<Node>();
    Node current = null;
    queue.offer(root); // 根节点入队
    while (!queue.isEmpty()) { // 只要队列中有元素,就可以一直执行,非常巧妙地利用了队列的特性
        current = queue.poll(); // 出队队头元素
        System.out.print("-->" + current.data);
        // 左子树不为空,入队
        if (current.leftChild != null)
            queue.offer(current.leftChild);
        // 右子树不为空,入队
        if (current.rightChild != null)
            queue.offer(current.rightChild);
    }
}

哈希表

哈希表名字源于 Hash,也可以叫作散列表。哈希表是一种特殊的数据结构,它与数组、链表以及树等我们之前学过的数据结构相比,有很明显的区别。

1)线性表中的栈和队列对增删有严格要求,它们会更关注数据的顺序。

2)数组和字符串需要保持数据类型的统一,并且在基于索引的查找上会更有优势。

3)树的优势则体现在数据的层次结构上。

但它们普遍都存在这样的缺陷,那就是数据数值条件的查找,都需要对全部数据或者部分数据进行遍历。

hash表的增删改查时间复杂度都是o(1)

哈希表的核心思想

哈希表的设计采用了函数映射的思想,将记录的存储位置与记录的关键字关联起来。这样的设计方式,能够快速定位到想要查找的记录,而且不需要与表中存在的记录的关键字比较后再来进行查找。

如果有一种方法,可以实现“地址 = f (关键字)”的映射关系,那么就可以快速完成基于数据的数值的查找了。这就是哈希表的核心思想。

如何设计哈希函数

第一,直接定制法

哈希函数为关键字到地址的线性函数。如,H (key) = a*key + b。 这里,a 和 b 是设置好的常数。

第二,数字分析法

假设关键字集合中的每个关键字 key 都是由 s 位数字组成(k1,k2,…,Ks),并从中提取分布均匀的若干位组成哈希地址。上面张一、张二、张三、张四的手机号信息存储,就是使用的这种方法。

第三,平方取中法

如果关键字的每一位都有某些数字重复出现,并且频率很高,我们就可以先求关键字的平方值,通过平方扩大差异,然后取中间几位作为最终存储地址。

第四,折叠法

如果关键字的位数很多,可以将关键字分割为几个等长的部分,取它们的叠加和的值(舍去进位)作为哈希地址。

第五,除留余数法

预先设置一个数 p,然后对关键字进行取余运算。即地址为 key mod p。

如何解决哈希冲突

第一,开放定址法

即当一个关键字和另一个关键字发生冲突时,使用某种探测技术在哈希表中形成一个探测序列,然后沿着这个探测序列依次查找下去。当碰到一个空的单元时,则插入其中。

常用的探测方法是线性探测法:

第二,链地址法

将哈希地址相同的记录存储在一张线性链表中。

例如,有一组关键字 {12,13,25,23,38,84,6,91,34},采用的哈希函数为 key mod 11。如下图所示:

哈希表的基本操作

在很多高级语言中,哈希函数、哈希冲突都已经在底层完成了黑盒化处理,是不需要开发者自己设计的。也就是说,哈希表完成了关键字到地址的映射,可以在常数级时间复杂度内通过关键字查找到数据。

实现细节,比如用了哪个哈希函数,用了什么冲突处理,甚至某个数据记录的哈希地址是多少,都是不需要开发者关注的。

哈希表的案例

假设有一个在线系统,可以实时接收用户提交的字符串型关键字,并实时返回给用户累积至今这个关键字被提交的次数。

例如,用户输入"abc",系统返回 1。用户再输入"jk",系统返回 1。用户再输入"xyz",系统返回 1。用户再输入"abc",系统返回 2。用户再输入"abc",系统返回 3。

一种解决方法是,用一个数组保存用户提交过的所有关键字。当接收到一个新的关键字后,插入到数组中,并且统计这个关键字出现的次数。

根据数组的知识可以计算出,插入到最后的动作,时间复杂度是 O(1)。但统计出现次数必须要全部数据遍历一遍,时间复杂度是 O(n)。随着数据越来越多,这个在线系统的处理时间将会越来越长。显然,这不是一个好的方法。

如果采用哈希表,则可以利用哈希表新增、查找的常数级时间复杂度,在 O(1) 时间复杂度内完成响应。预先定义好哈希表后(可以采用 Map < String, Integer > d = new HashMap <> (); )对于关键字(用变量 key_str 保存),判断 d 中是否存在 key_str 的记录。

如果存在,则把它对应的value(用来记录出现的频次)加 1;

如果不存在,则把它添加到 d 中,对应的 value 赋值为 1。最后,打印处 key_str 对应的 value,即累积出现的频次。

if (d.containsKey(key_str) {
    d.put(key_str, d.get(key_str) + 1);
}
else{
    d.put(key_str, 1);
}
System.out.println(d.get(key_str));


相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
目录
相关文章
|
1月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
49 1
|
2天前
|
存储 Java Serverless
【数据结构】哈希表&二叉搜索树详解
本文详细介绍了二叉搜索树和哈希表这两种数据结构。二叉搜索树是一种特殊二叉树,具有左子树节点值小于根节点、右子树节点值大于根节点的特点,并且不允许键值重复。文章给出了插入、删除和搜索等方法的具体实现。哈希表则通过哈希函数将键名映射为数组下标,实现快速查找,其插入、删除和查找操作时间复杂度理想情况下为O(1)。文中还讨论了哈希函数的设计原则、哈希冲突的解决方法及哈希表的实现细节。
17 8
【数据结构】哈希表&二叉搜索树详解
|
14天前
|
存储 机器学习/深度学习
【数据结构】二叉树全攻略,从实现到应用详解
本文介绍了树形结构及其重要类型——二叉树。树由若干节点组成,具有层次关系。二叉树每个节点最多有两个子树,分为左子树和右子树。文中详细描述了二叉树的不同类型,如完全二叉树、满二叉树、平衡二叉树及搜索二叉树,并阐述了二叉树的基本性质与存储方式。此外,还介绍了二叉树的实现方法,包括节点定义、遍历方式(前序、中序、后序、层序遍历),并提供了多个示例代码,帮助理解二叉树的基本操作。
38 13
【数据结构】二叉树全攻略,从实现到应用详解
|
9天前
|
C语言
数据结构基础详解(C语言):图的基本概念_无向图_有向图_子图_生成树_生成森林_完全图
本文介绍了图的基本概念,包括图的定义、无向图与有向图、简单图与多重图等,并解释了顶点度、路径、连通性等相关术语。此外还讨论了子图、生成树、带权图及几种特殊形态的图,如完全图和树等。通过这些概念,读者可以更好地理解图论的基础知识。
|
11天前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
11天前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
9天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
12天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
14 0
【数据结构】栈和队列的深度探索,从实现到应用详解