Java版-速通数据结构-树基础知识

news2024/12/26 1:14:10

现在面试问mysql,红黑树好像都是必备问题了。动不动就让手写红黑树或者简单介绍下红黑树。然而,我们如果直接去看红黑树,可能会一下子蒙了。在看红黑树之前,需要先了解下树的基础知识,从简单到复杂,看看红黑树是在什么场景下出现的,是哪种东西。
本文主要是介绍二叉树,二叉搜索树,然后到高度平衡二叉树,根据树的基本操作和特点,帮助理解那些特殊结构的树,是怎样演化而来的。

二叉树(Binary tree)基本概念

二叉树(Binary tree)是树形结构的一个重要类型。看它这名字,就是最多有俩叉的一种特殊的树形结构。通常,它的俩叉分别叫做左子树右子树
对二叉树的结构定义如下:

public class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int x) {
        val = x;
    }
}

二叉树的遍历

在这里插入图片描述

前序遍历

前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
例如,对于如上图二叉树,访问的先后顺序依次是:FBADCEGIH
下面使用简单递归来写个:

//前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        generate(root, result);
        return result;
    }

    private void generate(TreeNode root, List<Integer> result) {
        if (root == null) {
            return;
        }
        result.add(root.val);
        if (root.left == null && root.right == null) {
            return;
        }
        if (root.left != null) {
            generate(root.left, result);
        }
        if (root.right != null) {
            generate(root.right, result);
        }
    }

中序遍历

中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树。
例如,对于如上图二叉树,访问的先后顺序依次是:ABCDEFGHI

public List<Integer> inorderTraversal(TreeNode root) {
      List<Integer> result = new ArrayList<>();
      inorderTraversal(root, result);
      return result;
  }

  /**
   * 递归方式求解
   *
   * @param root
   * @param result
   */
  void inorderTraversal(TreeNode root, List<Integer> result) {
      if (root.left != null) {
          inorderTraversal(root.left, result);
      }
      result.add(root.val);
      if (root.right != null) {
          inorderTraversal(root.right, result);
      }
  }

后序遍历

后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点。
例如,对于如上图二叉树,访问的先后顺序依次是:ACEDBHIGF

public List<Integer> postorderTraversal(TreeNode root) {
       List<Integer> result = new ArrayList<>();
       generate(root, result);
       return result;
   }

   /**
    * 递归方法
    *
    * @param root
    * @param result
    */
   private void generate(TreeNode root, List<Integer> result) {
       //后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点
       if (root == null) {
           return;
       }
       if (root.left != null) {
           generate(root.left, result);
       }
       if (root.right != null) {
           generate(root.right, result);
       }
       result.add(root.val);
   }

层次遍历

层序遍历就是逐层遍历树结构。
例如,对于如上图二叉树,访问的先后顺序依次是:FBGADICEH

public List<List<Integer>> levelOrder(TreeNode root) {
       List<List<Integer>> result = new ArrayList<>();
       if (root == null) {
           return result;
       }
       Queue<TreeNode> queue = new LinkedList<>();
       queue.add(root);
       while (!queue.isEmpty()) {
           int size = queue.size();
           List<Integer> item = new ArrayList<>();
           for (int i = 0; i < size; i++) {
               TreeNode n = queue.poll();
               item.add(n.val);
               if (n.left != null) {
                   queue.add(n.left);
               }
               if (n.right != null) {
                   queue.add(n.right);
               }
           }
           result.add(item);
       }
       return result;
   }

二叉树的深度

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
是不是通过上面对于二叉树的遍历,发现,二叉树用递归方法,简直是太好写了,下面我们对这个深度的求解也使用递归:


    int answer = 0;

    public int maxDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int depth = 1;
        depth(root, depth);
        return answer;
    }

    private void depth(TreeNode root, int depth) {
        if (root.left == null && root.right == null) {
            answer = Math.max(answer, depth);
        }
        if (root.left != null) {
            depth(root.left, depth + 1);
        }
        if (root.right != null) {
            depth(root.right, depth + 1);
        }
    }

到这里我们会发现,对于二叉树的遍历操作,几乎都可以用递归来解决,超简单呀。

二叉搜索树(BinarySearchTree)

BST 定义

BST是二叉树的一种特殊表示形式,它满足如下特性:

  • 1,每个节点中的值必须大于(或等于)存储在其左侧子树中的任何值
  • 2,每个节点中的值必须小于(或等于)存储在其右子树中的任何值

我们可以把BST看成是进化了的二叉树。而且观察BST的这个特点,是不是让你想起来我们之前说过的数组的二分法,利用二分法对有序数组进行查找,可以提高搜索效率。如果对BST进行搜索,我们也可以充分利用BST的特征。

验证BST

对于二叉树,我们可以进行中序遍历(左中右),观察遍历得到的值是不是从小到大排列,可以使用此点验证二叉搜索树;

LinkedList<Integer> data = new LinkedList<>();

    public boolean isValidBST(TreeNode root) {
        if (root == null) {
            return true;
        }
        if (root.left != null && !isValidBST(root.left)) {
            return false;
        }
        //左中右遍历,让值依次增大即可
        if (!data.isEmpty()) {
            Integer n = data.peekLast();
            if (n >= root.val) {
                return false;
            }
        }
        data.add(root.val);
        if (root.right != null && !isValidBST(root.right)) {
            return false;
        }
        return true;
    }

BST基本操作

搜索

搜索的具体思路跟二分法也是蜜汁类似,不懂二分法的请翻看我以前写的关于数组的基本操作。
对于BST来说,如果当前比较数值过小,往右搜索,过大,往左搜索。

public TreeNode searchBST(TreeNode root, int val) {
      if (root == null) {
          return null;
      }
      if (root.val == val) {
          return root;
      }
      if (root.val > val && root.left != null) {
          return searchBST(root.left, val);
      }
      if (root.val < val && root.right != null) {
          return searchBST(root.right, val);
      }
      return null;
  }

插入

对于插入操作也是一样的,在比较的基础上,找到合适的位置,哈哈。

public TreeNode insertIntoBST(TreeNode root, int val) {
       if (root == null) {
           return root;
       }
       if (root.val > val && root.left == null) {
           root.left = new TreeNode(val);
           return root;
       }
       if (root.val < val && root.right == null) {
           root.right = new TreeNode(val);
           return root;
       }
       if (root.left != null && root.val > val) {
           insertIntoBST(root.left, val);
       }
       if (root.right != null && root.val < val) {
           insertIntoBST(root.right, val);
       }
       return root;
   }

删除

对于删除操作,可能比上面两种操作相对复杂一点。

    1. 如果目标节点没有子节点,我们可以直接移除该目标节点。
    1. 如果目标节只有一个子节点,我们可以用其子节点作为替换。
    1. 如果目标节点有两个子节点,我们需要用其中序后继节点或者前驱节点来替换,再删除该目标节点。

对于 1 和 2,很好理解。对于3,我们先来看一个结点,值的大小顺序为:左<中<右,如果我们要删除中间结点并且还要保持这个顺序不变,则我们有两个方法:1,使用左侧树的最大值去掉中间结点;2,使用右侧最小值取代中间结点。这样才能使得转变之后的树还满足BST的特征。
代码如下:

    /**
     * @param root
     * @param key
     * @return
     */
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) {
            return root;
        }
        if (root.val == key) {
            if (root.left == null && root.right == null) {
                return null;
            }
            if (root.left == null) {
                root = root.right;
                return root;
            }
            if (root.right == null) {
                root = root.left;
                return root;
            }
            root = getLeftChildMaxNode(root);
            return root;
        }
        if (root.val > key) {
            root.left = deleteNode(root.left, key);
            return root;
        }
        if (root.val < key) {
            root.right = deleteNode(root.right, key);
            return root;
        }
        return root;
    }

    /**
     * 转变右子树最小结点
     *
     * @param root
     * @return
     */
    private TreeNode getRightChildMinNode(TreeNode root) {
        if (root == null) {
            return root;
        }
        TreeNode left = root.left;
        TreeNode right = root.right;
        if (right.left == null) {
            right.left = left;
            return right;
        }
        //root的left直接挪到root.left的最小结点上
        TreeNode temp = right.left;
        while (temp.left != null) {
            temp = temp.left;
        }
        temp.left = left;
        return right;
    }

    /**
     * 转变左子树最大结点
     *
     * @return
     */
    private TreeNode getLeftChildMaxNode(TreeNode root) {
        if (root == null) {
            return root;
        }
        TreeNode left = root.left;
        TreeNode right = root.right;
        if (left.right == null) {
            left.right = right;
            return left;
        }
        TreeNode temp = left.right;
        while (temp.right != null) {
            temp = temp.right;
        }
        temp.right = right;
        return left;
    }

高度平衡二叉树

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
如果二叉搜索树的高度为 h ,则时间复杂度为 O(h) 。所以,二叉搜索树的高度的确很重要。对于一个有N个结点,高度为h的二叉树,h>= l o g 2 n {log_2{n}} log2n。对于具有 N 个节点的二叉搜索树的高度在 logN 到 N 区间变化。也就是说,搜索操作的时间复杂度可以从 logN 变化到 N 。这是一个巨大的性能差异。所以,我们应该尽量把二叉搜索树,往高度平衡的二叉搜索树上靠,来提高搜索效率。

高度平衡二叉树验证

emm,还是递归,超简单,按照定义写即可:

Boolean res = true;

   public boolean isBalanced(TreeNode root) {
       singleBalanced(root);
       return res;
   }

   int singleBalanced(TreeNode root) {
       if (root == null) {
           return 0;
       }
       int left = singleBalanced(root.left) + 1;
       int right = singleBalanced(root.right) + 1;
       if (Math.abs(left - right) > 1) {
           res = false;
       }
       return Math.max(left, right);
   }

有序数组转换成高度平衡二叉搜索树

我们取数组的中间元素作为根结点, 将数组分成左右两部分,对数组的两部分用递归的方法分别构建左右子树。
感觉其实是二分法的反作用。
这个可以用于对于普通二叉搜索树,先用中序遍历,生成有序数组,之后,将有序数组构建成高度平衡二叉树。
这是采用拆解重构建的方式构造高度平衡二叉树的一种方法。之后,我们还会介绍通过调整普通二叉搜索树,构建高度平衡二叉树或者近似于高度平衡二叉树的方法。

   public TreeNode sortedArrayToBST(int[] nums) {
       if (nums == null || nums.length == 0) {
           return null;
       }
       return build(nums, 0, nums.length - 1);
   }

   TreeNode build(int[] nums, int left, int right) {
       if (left == right) {
           return new TreeNode(nums[left]);
       }
       int mid = (left + right) / 2;
       TreeNode root = new TreeNode(nums[mid]);
       if (left + 1 == right) {
           root.right = new TreeNode(nums[right]);
           return root;
       }
       if (left + 2 == right) {
           root.left = new TreeNode(nums[left]);
           root.right = new TreeNode(nums[right]);
           return root;
       }
       root.left = build(nums, left, mid - 1);
       root.right = build(nums, mid + 1, right);
       return root;
   }

N叉树

N叉树:一个结点有N个叉,哈哈。
二叉树属于N叉树的一个特例。
结点定义:

class Node {
       public int val;
       public List<Node> children;

       public Node() {
       }

       public Node(int _val, List<Node> _children) {
           val = _val;
           children = _children;
       }
   }

N叉树的遍历

前序遍历


    public List<Integer> preorder(Node root) {
        List<Integer> res = new ArrayList<>();
        if (root == null) {
            return res;
        }
        res.add(root.val);
        if (root.children == null || root.children.size() < 1) {
            return res;
        }
        for (Node n : root.children) {
            List<Integer> items = preorder(n);
            res.addAll(items);
        }
        return res;
    }

后序遍历

/**
    * 递归方法
    *
    * @param root
    * @return
    */
   public List<Integer> postorder(Node root) {
       List<Integer> res = new ArrayList<>();
       if (root == null) {
           return res;
       }
       postorder(root, res);
       return res;
   }

   void postorder(Node root, List<Integer> res) {
       if (root.children == null) {
           res.add(root.val);
           return;
       }
       for (Node n : root.children) {
           postorder(n, res);
       }
       res.add(root.val);
   }

层序遍历

public List<List<Integer>> levelOrder(Node root) {
      List<List<Integer>> res = new ArrayList<>();
      if (root == null) {
          return res;
      }
      Queue<Node> queue = new LinkedList<>();
      queue.add(root);
      while (!queue.isEmpty()) {
          int size = queue.size();
          List<Integer> data = new ArrayList<>();
          for (int i = 0; i < size; i++) {
              Node node = queue.poll();
              data.add(node.val);
              if (node.children != null && node.children.size() > 0) {
                  queue.addAll(node.children);
              }
          }
          res.add(data);
      }
      return res;
  }

前缀树

前缀树定义

在这里插入图片描述

前缀树特点:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符;
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串;
  • 每个节点的所有子节点包含的字符都不相同。

下面为两种常用前缀树的结构:
1,使用数组来存储后缀结点:

class TrieNode {
    public static final int N = 26;
    public TrieNode[] children = new TrieNode[N];
    // ......

}

2,使用map来存储后缀结点:

class TrieNode {
    public Map<Character, TrieNode> children = new HashMap<>();

    // ......
};

实现前缀树的插入和搜索

class Trie {

       private Map<Character, TrieNode> data = new HashMap<>();


       public Trie() {

       }


       public void insert(String word) {
           if (word == null || word.isEmpty()) {
               return;
           }
           if (search(word)) {
               return;
           }
           char[] words = word.toCharArray();
           char head = words[0];
           TrieNode currt = data.get(head);
           if (currt == null) {
               currt = new TrieNode(head);
               data.put(head, currt);
           }
           for (int i = 1; i < words.length; i++) {
               char c = words[i];
               if (currt.next == null) {
                   currt.next = new ArrayList<>();
               }
               TrieNode target = currt.next.stream().filter(n -> n.c == c).findFirst().orElse(null);
               if (target == null) {
                   target = new TrieNode(c);
                   currt.next.add(target);
               }
               currt = target;
           }
           currt.isWord = true;
       }


       public boolean search(String word) {
           if (word == null || word.isEmpty()) {
               return false;
           }
           char[] words = word.toCharArray();
           char head = words[0];
           TrieNode currt = data.get(head);
           if (currt == null) {
               return false;
           }
           for (int i = 1; i < words.length; i++) {
               char c = words[i];
               if (currt.next == null || currt.next.size() == 0) {
                   return false;
               }
               List<TrieNode> next = currt.next;
               TrieNode t = next.stream().filter(n -> n.c == c).findFirst().orElse(null);
               if (t == null) {
                   return false;
               }
               currt = t;
           }
           return currt.isWord;
       }


       public boolean startsWith(String prefix) {
           if (prefix == null || prefix.isEmpty()) {
               return false;
           }
           char[] words = prefix.toCharArray();
           char head = words[0];
           TrieNode currt = data.get(head);
           if (currt == null) {
               return false;
           }
           for (int i = 1; i < words.length; i++) {
               char c = words[i];
               if (currt.next == null || currt.next.size() == 0) {
                   return false;
               }
               List<TrieNode> next = currt.next;
               TrieNode t = next.stream().filter(n -> n.c == c).findFirst().orElse(null);
               if (t == null) {
                   return false;
               }
               currt = t;
           }
           return true;
       }
   }

小结

本文从最简单的二叉树开始讲起,介绍了简单二叉树的遍历,
之后延伸到对于搜索友好的二叉搜索树,对比二叉搜索树和我之前chat里面讲过的二分法,你会发现,提高搜索效率的秘诀,在于构建有序的结构,之后尽量利用二分法的原理,使得搜索的时间复杂度靠近 l o g 2 n log_2n log2n
但是在实际操作中,我们会发现,将树维护在高度平衡内,实在是要耗费的力气太大了,于是不得不在高度平衡和构建树上做一个妥协,由此衍生出了很多工业级别的树,但是限于本文篇幅,这里没有涉及,或许我会在后续的chat安排上。

除了二叉树,本文还简单介绍了下N叉树和前缀树,简单了解下树的其他应用方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2254347.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

浙江工业大学《2024年828自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《浙江工业大学828自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2024年真题 Part1&#xff1a;2024年完整版真题 2024年真题

【计算机网络】实验11:边界网关协议BGP

实验11 边界网关协议BGP 一、实验目的 本次实验旨在验证边界网关协议&#xff08;BGP&#xff09;的实际作用&#xff0c;并深入学习在路由器上配置和使用BGP协议的方法。通过实验&#xff0c;我将探索BGP在不同自治系统之间的路由选择和信息交换的功能&#xff0c;理解其在互…

微信小程序全屏显示地图

微信小程序在界面上显示地图&#xff0c;只需要用map标签 <map longitude"经度度数" latitude"纬度度数"></map>例如北京的经纬度为&#xff1a;116.407004,39.904595 <map class"bgMap" longitude"116.407004" lati…

InfluxDB 集成 Grafana

将InfluxDB集成到Grafana进行详细配置通常包括以下几个步骤&#xff1a;安装与配置InfluxDB、安装与配置Grafana、在Grafana中添加InfluxDB数据源以及创建和配置仪表板。以下是一个详细的配置指南&#xff1a; 一、安装与配置InfluxDB 下载与安装&#xff1a; 从InfluxDB的官…

【AI系统】ESPNet 系列

ESPNet 系列 本文将会介绍 ESPNet 系列&#xff0c;该网络主要应用在高分辨率图像下的语义分割&#xff0c;在计算内存占用、功耗方面都非常高效&#xff0c;重点介绍一种高效的空间金字塔卷积模块&#xff08;ESP Module&#xff09;&#xff1b;而在 ESPNet V2 上则是会更进…

【Axios】如何在Vue中使用Axios请求拦截器

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

w~深度学习~合集1

我自己的原文哦~ https://blog.51cto.com/whaosoft/12663254 #Motion Plan 代码 github.com/liangwq/robot_motion_planing 轨迹约束中的软硬约束 前面的几篇文章已经介绍了&#xff0c;轨迹约束的本质就是在做带约束的轨迹拟合。输入就是waypoint点list&#xff0c;约束…

大语言模型应用开发框架LangChain

大语言模型应用开发框架LangChain 一、LangChain项目介绍1、简介2、LangChain的价值3、实战演练 二、LangChain提示词大语言模型应用1、简介1.1、提示词模板化的优点1.2、提示词模板LLM 的应用1.3、Prompt 2、应用实战2.1、PromptTemplate LLM2.2、PromptTemplate LLM Outpu…

公众号文章标题的重要性

标题&#xff0c;不仅仅是一个简单的标题&#xff0c;它更是吸引读者眼球的“颜值担当”。 信息爆炸的今天&#xff0c;一个好的标题就是打开流量之门的金钥匙。那么&#xff0c;如何衡量一个标题的“颜值”呢&#xff1f;我们可以从两个维度来看&#xff1a;打开率和传播率。…

116. UE5 GAS RPG 实现击杀掉落战利品功能

这一篇&#xff0c;我们实现敌人被击败后&#xff0c;掉落战利品的功能。首先&#xff0c;我们将创建一个新的结构体&#xff0c;用于定义掉落体的内容&#xff0c;方便我们设置掉落物。然后&#xff0c;我们实现敌人死亡时的掉落函数&#xff0c;并在蓝图里实现对应的逻辑&…

ros2人脸检测

第一步&#xff1a; 首先在工作空间/src下创建数据结构目录service_interfaces ros2 pkg create service_interfaces --build-type ament_cmake 然后再创建一个srv目录 在里面创建FaceDetect.srv&#xff08;注意&#xff0c;首字母要大写&#xff09; sensor_msgs/Image …

Neo4j:图数据库使用入门

文章目录 一、Neo4j安装1、windows安装&#xff08;1&#xff09;准备环境&#xff08;2&#xff09;下载&#xff08;3&#xff09;解压&#xff08;4&#xff09;运行&#xff08;5&#xff09;基本使用 2、docker安装 二、CQL语句1、CQL简介2、CREATE 命令&#xff0c;创建节…

五.指派问题

匈牙利发求解指派问题找独立0元素&#xff0c;常用的步骤为&#xff1a;

如何利用AI生成专业级海报教程:解决中文嵌入问题的实战指南

AI生成专业级海报教程:解决中文嵌入问题的实战指南 一、前言:突破性进展 重大突破!字节即梦AI最新发布的v2.1绘图模型完美解决了中文文字嵌入问题。等待了整整两年,我们终于等到了这一天 —— AI可以直接在图片上完美呈现中文字体,审美和泛化能力都达到了惊人的水平。 二…

优质翻译在美国电子游戏推广中的作用

美国作为世界上最大的视频游戏市场之一&#xff0c;为寻求全球成功的游戏开发商提供了无与伦比的机会。然而&#xff0c;美国市场的文化和语言多样性使其成为一个复杂的导航景观。高质量的翻译在弥合开发者和这些充满活力的观众之间的差距方面发挥着关键作用&#xff0c;确保游…

嵌入式驱动开发详解4(内核定时器)

文章目录 前言通用定时器系统节拍节拍数与时间转换基本框架定时器使用代码展示通用定时器特点 高精度定时器 前言 LInux内核定时器是一种基于未来时间点的计时方式&#xff0c;以当前时刻来启动的时间点&#xff0c;以未来的某一时刻为终止点。比如&#xff0c;现在是10点5分&…

力扣-图论-3【算法学习day.53】

前言 ###我做这类文章一个重要的目的还是给正在学习的大家提供方向和记录学习过程&#xff08;例如想要掌握基础用法&#xff0c;该刷哪些题&#xff1f;&#xff09;我的解析也不会做的非常详细&#xff0c;只会提供思路和一些关键点&#xff0c;力扣上的大佬们的题解质量是非…

2024年认证杯SPSSPRO杯数学建模D题(第一阶段)AI绘画带来的挑战解题全过程文档及程序

2024年认证杯SPSSPRO杯数学建模 D题 AI绘画带来的挑战 原题再现&#xff1a; 2023 年开年&#xff0c;ChatGPT 作为一款聊天型AI工具&#xff0c;成为了超越疫情的热门词条&#xff1b;而在AI的另一个分支——绘图领域&#xff0c;一款名为Midjourney&#xff08;MJ&#xff…

各种常见生信格式文件的随机抽样

样本检验、随机生成数据、模拟用等&#xff0c;都需要从现有测序数据中随机抽样出一小部分数据来&#xff0c;按照自己需求。 0&#xff0c;最经典的方式&#xff1a; 使用awk等&#xff0c;只要了解各种数据格式具体的行列组成&#xff08;一般是headerrecord&#xff09;&a…

【技展云端,引擎蓝天】2025涡轮展之民用航空发动机技术分论坛及展览展示

2023年全球航空发动机市场规模约为1139.72亿美元&#xff0c;预计到2030年将达到1511.95亿美元&#xff0c;年均复合增长率为4.12%。这主要得益于全球航空运输需求的不断增长、新兴市场的快速扩张以及更高效、更环保的发动机技术创新。 航空发动机是一种高度复杂和精密的热力机…