数据结构 02(上)

简介: 在《数据结构 01》一文中,说到了数组、链表、栈以及队列这几种基本的线性结构,接下来就一起来看看剩下的内容。

一、树形结构


1. 什么是树形结构?


树形结构是一层次的嵌套结构。 一个树形结构的外层和内层有相似的结构,表示的是一对多的关系。它和链表一样,也是一种动态的数据结构。


image.png


2. 为什么要有树形结构?


其实很多现实中存在的东西就是树形结构的,最典型的就是计算机中的文件夹。文件夹与文件夹之间有层次关系,一个文件夹下可以有若干个文件或者文件夹,这就是树形结构。


3. 二分搜索树:


3.1. 二叉树:


在说二分搜索树之前先说说二叉树。二叉树是一种特殊的树形结构,特殊在:


  • 每个节点最多有两棵子树。


  • 子树也有左右之分。


image.png

树也是用节点来存储元素,和链表不同的是,二叉树的节点类属性有三个,一个是泛型的e用来存储数据,一个是Node类型的left表示左节点,Node类型的right表示右节点。

class Node{
    E e;
    Node left;
    Node right;
}


二叉树具有天然的递归结构。因为二叉树的每个节点,都可以看成是子树的根节点。


3.2. 二分搜索树:


二分搜索树首先是一棵二叉树,同时具备以下特点:


  • 二分搜索树每个节点的值,都大于左子树的所有节点的值。
  • 二分搜索树每个节点的值,都小于右子树的所有节点的值。
  • 二分搜索树的每一棵子树也都是二分搜索树。


image.png


  • 这棵就是二分搜索树,每一个节点的值都大于左子树所有节点的值且小于右子树的所有节点的值。所以,二分搜索树存储的元素必须有可比较性。如果存储的是引用类型的数据,那么就应该用比较器指定某个属性进行比较。


3.2.1 实现二分搜索树:

public class BST<E extends Comparable<E>> {
    private class Node{//节点类
        public E e;//用来存储元素
        public Node left,right;//左孩子,右孩子
        public Node(E e){
            this.e = e;
            left = null;
            right = null;
        }
    }
    private Node root;//根节点
    private int size;//元素的个数
    public BST(){
        root = null;
        size = 0;
    }
}


根据上面的分析,可得上述代码。一棵二分搜索树用一个内部节点类来存储元素,同时有三个属性,左孩子、右孩子以及表示存储的元素个数的size。


3.2.2 二分搜索树的操作:


  • 基本操作:获取存储的元素个数以及判断是否为空

/** 获取二分搜索树中元素的个数 */
 public int size(){
     return size;
 }
 /** 判断二分搜索树是否为空 */
 public boolean isEmpty(){
     return size == 0;
 }


这两个方法比较简单,就不多说了。


  • 添加元素:


根据二分搜索树的性质,向其中添加元素,就需要从根节点开始判断添加的元素应该是在左边还是右边。假如比根节点元素更小,那就再与根节点的左孩子为根节点,再次进行判断。所以可以用递归来实现。

/** 向二分搜索树中添加元素(改进版) */
 public void add(E e){
     root = add(root,e);
 }
 //向二分搜索树中添加元素,返回根节点
 private Node add(Node node,E e){
        if (node == null){           //递归
            size ++;                 //终止
            return new Node(e);      //条件
        }
        if (e.compareTo(node.e) < 0){
            node.left = add(node.left,e);
        }else if (e.compareTo(node.e) > 0){
            node.right = add(node.right,e);
        }
        return node;
 }


给用户提供的是public的add方法,真正执行递归添加操作的是private的这个方法。从宏观上来讲,首先把root根节点传给这个方法,如果根节点为空,直接将元素添加到根节点;如果根节点不为空,且要添加的元素比该节点元素小,那么就在左边执行添加操作,更大就在右边执行添加操作,相等的话就不添加。那么从微观角度来讲,代码具体是怎么运行的呢?比如现有如下二分搜索树:


image.png


现要把“16”这个元素添加到该树中。看看代码的运行过程:


image.png


首先看上图红色文字以及红色连线。在给用户提供的add方法中,传入root(14),e(16),16大于14,所以再次调用add方法,传入的当前node的右节点,14的右节点就是20;传入节点20,再次判断,发现16比20小,再次调用add并且传入20的左节点;到了图三,发现传入的20的左节点是null,所以new一个节点存储16(开始看紫色文字和紫色连线),并且把这个节点返回给图二的add方法处,并用节点20的left接收,这样新添加的节点16就成了节点20的左孩子;同时把节点20 return给图一的add方法处,并用节点14(root)的right接收,同时返回root,这样就完成了添加操作。添加完成后二分搜索树就变成:



image.png


  • 遍历二分搜索树(深度优先):

/** 二分搜索树的前序遍历 */
public void preOrder(){
    preOrder(root);
}
private void preOrder(Node node){
    if (node == null)
        return;
    System.out.println(node.e);
    preOrder(node.left);
    preOrder(node.right);
}
/** 二分搜索树中序遍历(遍历出来的就是排好序的) */
public void inOrder(){
    inOrder(root);
}
private void inOrder(Node node){
    if (node == null)
        return;
    inOrder(node.left);
    System.out.println(node.e);
    inOrder(node.right);
}
/** 二分搜索树后序遍历 */
public void postOrder(){
    postOrder(root);
}
private void postOrder(Node node){
    if (node == null)
        return;
    postOrder(node.left);
    postOrder(node.right);
    System.out.println(node.e);
}


遍历也是使用递归实现的,相对简单,就不画图解释了。


  • 层序遍历:


上面三种遍历方式叫深度优先遍历,还有一种广度优先的遍历,叫层序遍历。就是先遍历第一层,再遍历第二层……。


image.png


/** 二分搜索树层序遍历 */
 public void levelOrder(){
      Queue<Node> queue = new LinkedList<>();
      queue.add(root);
      while (!queue.isEmpty()){
          Node cur = queue.remove();
          System.out.println(cur.e);
          if (cur.left != null)
              queue.add(cur.left);
          if (cur.right != null)
              queue.add(cur.right);
      }
}


这里层序遍历借助了队列来实现,相对也比较好理解。


  • 删除操作:


首先来看两个简单的删除操作,即删除最小值和最大值。根据二分搜索树的性值,最小值一定是往左走的最后一个元素,最大值一定是往右走的最后一个元素。

/** 寻找二分搜索树的最小值 */
    public E minele(){
        if (size == 0)
            throw new IllegalArgumentException("二分搜索树为空");
        return minele(root).e;
    }
    private Node minele(Node node){
        if (node.left == null)
            return node;
        return minele(node.left);
    }
    /** 寻找二分搜索树的最大值 */
    public E maxele(){
        if (size == 0)
            throw new IllegalArgumentException("二分搜索树为空");
        return maxele(root).e;
    }
    private Node maxele(Node node){
        if (node.right == null)
            return node;
        return maxele(node.right);
    }
    /** 从二分搜索树中删除最小元素,并返回该元素 */
    public E removeMin(){
        E temp = minele();//最小元素
        root = removeMin(root);//删除最小元素
        return temp;//返回最小元素
    }
    private Node removeMin(Node node){
        if (node.left == null){
            Node rightNode = node.right;//保留右边的
            node.right = null;
            size --;
            return rightNode;
        }
        node.left = removeMin(node.left);
        return node;
    }
    /** 从二分搜索树中删除最大元素,并返回该元素 */
    public E removeMax(){
        E temp = minele();//最小元素
        root = removeMax(root);//删除最小元素
        return temp;//返回最小元素
    }
    private Node removeMax(Node node){
        if (node.right == null){
            Node leftNode = node.left;//保留右边的
            node.left = null;
            size --;
            return leftNode;
        }
        node.right = removeMax(node.right);
        return node;
    }


这就是删除最小值和最大值的代码,逻辑还是比较简单的。接下来看看删除任意一个元素。删除任意一个元素的话,如果这个元素有左子树和右子树,首先找到这个元素右子树中的最小值,用这个值取待被删除的元素,然后用这个元素左侧和右侧都连接上原来的子树就可以了。比如现在有如下二分搜索树,要删除元素58。


image.png


那么首先找到58的右子树中的最小值,即59,找到59后就把59从这个右子树中删除,同时用59取待58。就变成了这样:

image.png


这样就删除完成了。下面用代码来实现一下。

/** 删除二分搜索树中的元素e */
public void remove(E e){
    root = remove(root,e);
}
private Node remove(Node node,E e){
    if (node == null)
        return null;
    if (e.compareTo(node.e) < 0){
        node.left = remove(node.left,e);
        return node;
    }else if (e.compareTo(node.e) > 0){
        node.right = remove(node.right,e);
        return node;
    }else {
        if (node.left == null){
            Node rightNode = node.right;
            node.right = null;
            size --;
            return rightNode;
        }
        if (node.right == null){
            Node leftNode = node.left;
            node.left = null;
            size --;
            return leftNode;
        }
        Node successor = minele(node.right);
        successor.right = removeMin(node.right);
        successor.left = node.left;
        node.left = node.right = null;
        return successor;
    }
}


这段代码就对应了上面所分析的逻辑。




相关文章
|
存储 算法 Java
数据结构:八大常用数据结构
数据结构是计算机存储、组织数据的方式;通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构的优良将直接影响着我们程序的性能;常用的数据结构有:数组(Array)、栈(Stack)、队列(Queue)、链表(Linked List)、树(Tree)、图(Graph)、堆(Heap)、散列表(Hash)等;
19944 14
|
存储 Java 编译器
经验总结:源代码-目标代码的区别
源代码是由程序员用高级语言编写的可读文本文件,需编译成机器可执行的二进制目标代码。目标代码由编译器生成,包含机器指令,对机器可读但对人类不易理解。源代码便于修改,而目标代码需重新编译以反映更改。源代码不受系统限制,目标代码则特定于系统。此外,链接程序处理源文件间及库函数的引用,将目标文件连接成可执行文件。Java中的本地方法则允许调用非Java语言编写的代码,实现与底层系统的交互,提高程序性能或实现特定功能。
525 4
|
存储 搜索推荐 算法
【数据结构入门精讲 | 第九篇】考研408排序算法专项练习(一)
【数据结构入门精讲 | 第九篇】考研408排序算法专项练习(一)
1049 1
|
11天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
10天前
|
存储 人工智能 搜索推荐
终身学习型智能体
当前人工智能前沿研究的一个重要方向:构建能够自主学习、调用工具、积累经验的小型智能体(Agent)。 我们可以称这种系统为“终身学习型智能体”或“自适应认知代理”。它的设计理念就是: 不靠庞大的内置知识取胜,而是依靠高效的推理能力 + 动态获取知识的能力 + 经验积累机制。
352 131
|
10天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
443 131
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
4天前
|
存储 安全 前端开发
如何将加密和解密函数应用到实际项目中?
如何将加密和解密函数应用到实际项目中?
206 138
|
10天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
403 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)