当前位置: 首页 > news >正文

数据结构 二叉树和堆总结

概念

树是一种层次结构非线性的数据结构,其是由节点和边组成,可以用来表示层次关系的数据。

树的相关概念

  • 节点:树的基本组成单位,每个节点都包含数据,同时与其他节点相互连接
  • 根节点:树的顶层节点,没有父节点
  • 子节点:由一个节点直接连接的下层节点
  • 父节点:直接连接子节点的上层节点
  • 叶子节点:没有子节点的就是叶子节点
  • 内部节点:至少有一个子节点的节点
  • 兄弟节点:具有同一个父节点的节点
  • 路径:从一个节点到另外一个节点所经过的节点路径
  • 深度:节点到根节点的路径长度
  • 高度:节点到叶子节点的最长路径长度
  • 度:节点的子节点数
  • 子树:由某个节点机器所有后代节点组成的树

二叉树

概念

二叉树是一种特殊的树结构,其每个节点最多有两个子节点,也就是左子节点和右子节点,二叉树可以为空(没有节点,也可以只有一个根节点)。

二叉树性质

重要性质总结

  • 节点数与层数

    • 二叉树第iii层上最多有 2i−12^{i-1}2i−1 个节点。
    • 深度为 hhh 的二叉树最多有 2h−12^h - 12h−1 个节点。
  • 满二叉树

    • 深度为 hhh 且有 2h−12^h - 12h−1 个节点的二叉树称为满二叉树。
    • 满二叉树中每一层的节点数都达到最大。
  • 完全二叉树

    • 深度为 hhh 且有 nnn 个节点的二叉树,当且仅当其每一个节点都与深度为 hhh 的满二叉树中编号为 1 到 nnn 的节点对应时,称其为完全二叉树。
    • 完全二叉树除了最后一层外,每层都是满的,且最后一层的节点从左到右依次排列。
  • 节点的度

    • 二叉树中度为 0 的节点(即叶节点)的数量为 n0n_0n0​。
    • 度为 2 的节点数量为 n2n_2n2​。
    • 对于一个二叉树,满足关系 n0=n2+1n_0 = n_2 + 1n0​=n2​+1。
  • 二叉树的遍历

    • 前序遍历(Pre-order Traversal):先访问根节点,再访问左子树,最后访问右子树。
    • 中序遍历(In-order Traversal):先访问左子树,再访问根节点,最后访问右子树。
    • 后序遍历(Post-order Traversal):先访问左子树,再访问右子树,最后访问根节点。
    • 层次遍历(Level-order Traversal):按层次从上到下,从左到右依次访问各节点。
  • 子树

    • 每个节点的左子树和右子树也是二叉树。
  • 树的高度

    • 二叉树的高度是从根节点到叶节点的最长路径的节点数。
  • 平衡二叉树

    • 一种特殊的二叉树,任何一个节点的左右子树的高度差不超过 1。常见的平衡二叉树包括 AVL 树和红黑树。

二叉树存储结构

链式存储结构:一个节点中有一个表示数据的,以及左右节点指针

#include<iostream>struct TreeNode
{int data;TreeNode* left;TreeNode* right;TreeNode(int value):data(value),left(nullptr),right(nullptr){}
};TreeNode* CreateTree()
{TreeNode* root = new TreeNode(1);root->left = new TreeNode(2);root->right = new TreeNode(3);root->left->left = new TreeNode(4);root->left->right = new TreeNode(5);root->right->left = new TreeNode(6);root->right->right = new TreeNode(7);return root;
}void prevOrderTree(TreeNode* root)
{if (root == nullptr) return;std::cout << root->data << " ";prevOrderTree(root->left);prevOrderTree(root->right);
}int main()
{TreeNode* root = CreateTree();prevOrderTree(root);std::cout << std::endl;return 0;
}

顺序存储结构:使用一个数组进行存储,针对于第i个节点,2i位置是左子树,2i+1节点是右子树(基本不用,只适合完全静态的树)

#include <iostream>
#include <vector>
#include <cmath>std::vector<int> createBinaryTreeArray() {// 假设二叉树的深度为3std::vector<int> tree(8);  // 大小为2^3 = 8,index从0开始tree[1] = 1;  // 根节点tree[2] = 2;  // 左子节点tree[3] = 3;  // 右子节点tree[4] = 4;  // 左子节点的左子节点tree[5] = 5;  // 左子节点的右子节点tree[6] = 6;  // 右子节点的左子节点tree[7] = 7;  // 右子节点的右子节点return tree;
}void preOrderTraversalArray(const std::vector<int>& tree, int index) {if (index >= tree.size() || tree[index] == 0) return;std::cout << tree[index] << " ";preOrderTraversalArray(tree, 2 * index);     // 左子节点preOrderTraversalArray(tree, 2 * index + 1); // 右子节点
}int main() {std::vector<int> tree = createBinaryTreeArray();std::cout << "Pre-order traversal: ";preOrderTraversalArray(tree, 1);  // 从根节点开始遍历std::cout << std::endl;return 0;
}

前中后序遍历

总结

  • 前序遍历:根--左--右
  • 后序遍历:左--右--根
  • 中序遍历:左--根--右

#include<iostream>struct TreeNode
{int data;TreeNode* left;TreeNode* right;TreeNode(int value):data(value),left(nullptr),right(nullptr){}
};TreeNode* CreateTree()
{TreeNode* root = new TreeNode(1);root->left = new TreeNode(2);root->right = new TreeNode(3);root->left->left = new TreeNode(4);root->left->right = new TreeNode(5);root->right->left = new TreeNode(6);root->right->right = new TreeNode(7);return root;
}void prevOrderTree(TreeNode* root)
{if (root == nullptr) return;std::cout << root->data << " ";prevOrderTree(root->left);prevOrderTree(root->right);
}void inOrderTree(TreeNode* root)
{if (root == nullptr) return;inOrderTree(root->left);std::cout << root->data << " ";inOrderTree(root->right);
}void postOrderTree(TreeNode* root)
{if (root == nullptr) return;postOrderTree(root->left);postOrderTree(root->right);std::cout << root->data << " ";
}int main()
{TreeNode* root = CreateTree();std::cout << "前序遍历:";prevOrderTree(root);std::cout << std::endl;std::cout << "中序遍历:";inOrderTree(root);std::cout << std::endl;std::cout << "后序遍历:";postOrderTree(root);std::cout << std::endl;return 0;
}

 层序遍历

实现方式:借助队列实现

  • 初始化队列,将根节点的数值放入进去
  • 设置循环直到队列为空: 队列中提取并出队,然后分别放入左右的数值进行循环判断

#include<iostream>
#include<queue>struct TreeNode
{int data;TreeNode* left;TreeNode* right;TreeNode(int value):data(value),left(nullptr),right(nullptr){}
};TreeNode* CreateTree()
{TreeNode* root = new TreeNode(1);root->left = new TreeNode(2);root->right = new TreeNode(3);root->left->left = new TreeNode(4);root->left->right = new TreeNode(5);root->right->left = new TreeNode(6);root->right->right = new TreeNode(7);return root;
}void levelOrderTree(TreeNode* root)
{if (!root) return;std::queue<TreeNode*>q;q.push(root);while (!q.empty()){TreeNode* tem = q.front();q.pop();std::cout << tem->data << " ";if (tem->left) q.push(tem->left);if (tem->right) q.push(tem->right);}
}int main()
{TreeNode* root = CreateTree();levelOrderTree(root);std::cout << std::endl;return 0;
}

存储方式

  • 通常借助数组来存储
    • 父节点下标:(i -1)/2
    • 左孩子:2i + 1
    • 右孩子:2i + 2

概念

  • 堆是一颗完全二叉树,除了最后一层,所有层都是满的,并且最后一层的节点是从左往右依次排列
  • 堆主要有两种表现形式
    • 大堆:每个节点的值都大于等于其子节点的值
    • 小堆:每个节点的值都小于等于其节点的值

向上调整与向下调整

向上调整

  • 插入新元素时,将新元素放入到堆的末尾
  • 从新插入元素的下标(堆的末尾),检查父节点是否比自己大或者小(根据大小堆来决定,大堆的话则是当前元素比父元素大就交换)
  • 如果大于或者小于父节点,则交换两个节点,然后当前下标更新成为父元素的下标,继续检查
  • 重复此过程,直到当前元素不大于父元素,或者已经到达栈顶
  • 事例代码(建大堆)
  • 场景:当插入新元素的时候使用向上调整,因为新元素是插入到数组尾部
	//向上调整void AdjuestUp(int index){while (index > 0 && heap[index] > heap[(index - 1) / 2]){std::swap(heap[index], heap[(index - 1) / 2]);index = (index - 1) / 2;}}

 向下调整

  • 从堆中删除第一个元素(大堆就是删除最大元素,小堆则是最小)下面叙述按照大堆逻辑。将堆的最后一个元素移动到堆顶
  • 从堆顶开始,找到当前节点、左孩子、右孩子三个节点的最大值
  • 如果当前节点不是最大值,则将其与最大值节点交换,并更新当前索引为最大子节点索引,继续检查
  • 重复上述过程,直到当前节点大于或者等于其子节点,或者已经到达堆的底部
  • 使用场景:删除元素的时候
  • 代码事例,大堆的向下调整
//改写法适合排序堆排使用void AdjustDown(vector<int>& arr, int n, int i)
{int largest = i;int left = 2 * i + 1, right = 2 * i + 2;if (left<n && arr[left]>arr[largest]) largest = left;if (right<n && arr[right]>arr[largest]) largest = right;if (largest != i){swap(arr[i], arr[largest]);AdjustDown(arr, n, largest);}
}
	//向下调整void AdjuestDown(int index){int size = heap.size();while (index * 2 + 1 < size){int largest = index;int leftChild = index * 2 + 1;int rightChild = index * 2 + 2;//找到是三个节点的最大值if (leftChild < size && heap[leftChild]>heap[largest]){largest = leftChild;}if (rightChild<size && heap[rightChild]>heap[largest]){largest = rightChild;}//如果最大节点不是当前节点,则交换最大值,然后继续向下调整if (largest != index){std::swap(heap[index], heap[largest]);index = largest;}else{break;}}}

时间复杂度分析

堆操作相关时间复杂度分析

  • 插入操作(Insert)

    • 时间复杂度:O(log n)
    • 插入一个新元素时,首先将其添加到堆的末尾,然后通过向上调整(heapify up)使堆恢复最大堆或最小堆的性质。向上调整的过程最多需要经过堆的高度层,而堆的高度是 O(logn)O(log n)O(logn)。
  • 删除堆顶元素(Extract Max / Extract Min)

    • 时间复杂度:O(log n)
    • 删除堆顶元素(即最大堆中的最大元素或最小堆中的最小元素)时,将堆的最后一个元素移动到堆顶,然后通过向下调整(heapify down)使堆恢复堆的性质。向下调整的过程最多需要经过堆的高度层,时间复杂度也是 O(logn)O(log n)O(logn)。
  • 获取堆顶元素(Get Max / Get Min)

    • 时间复杂度:O(1)
    • 获取堆顶元素不需要进行任何调整操作,只需要返回堆数组的第一个元素即可,因此时间复杂度是 O(1)O(1)O(1)。
  • 建堆操作(Build Heap)

    • 时间复杂度:O(n)
    • 从一个无序数组构建一个堆可以通过自底向上的方法进行,即从最后一个非叶子节点开始向下调整(heapify down)。这种方法的总时间复杂度是 O(n)O(n)O(n),而不是 O(nlog⁡n)O(n \log n)O(nlogn),因为每个节点向下调整的平均次数比堆的高度要少。
  • 堆排序(Heap Sort)

    • 时间复杂度:O(n \log n)
    • 堆排序包括两个主要步骤:首先将无序数组构建成一个堆(时间复杂度 O(n)O(n)O(n)),然后重复地提取堆顶元素并调整堆(每次提取和调整的时间复杂度是 O(logn)O(log n)O(logn))。因此,总的时间复杂度是 O(n)+O(nlog⁡n)=O(nlog⁡n)O(n) + O(n \log n) = O(n \log n)O(n)+O(nlogn)=O(nlogn)。

堆排序

思路总结(以大堆为例分析)

  • 构建大堆,将数组转化成为堆
  • 交换堆顶元素与末尾元素,同时缩小堆的范围,对堆顶元素进行向下调整,直到数组排序完成

 

#include<iostream>
#include<vector>
#include<algorithm>using namespace std;void AdjustDown(vector<int>& arr, int n, int i)
{int largest = i;int left = 2 * i + 1, right = 2 * i + 2;if (left<n && arr[left]>arr[largest]) largest = left;if (right<n && arr[right]>arr[largest]) largest = right;if (largest != i){swap(arr[i], arr[largest]);AdjustDown(arr, n, largest);}
}void heapSort(vector<int>& arr)
{int n = arr.size();//构建大堆(找父节点然后调整)for (int i = n / 2 - 1; i >= 0; i--){AdjustDown(arr, n, i);}//交换头尾元素,然后再次向下调整for (int i = n - 1; i > 0; i--){swap(arr[0], arr[i]);AdjustDown(arr, i, 0);}
}int main()
{std::vector<int> arr{ 12,89,5,4,66,82,56,99,1 };cout << "begin arr:";for (auto& ch : arr) {cout << ch << " ";}cout << endl;heapSort(arr);cout << "after sort :";for (auto& ch : arr){cout << ch << " ";}
}

堆的简单实现

插入与删除实现

  • 插入:末尾插入新元素,然后向上调整
  • 删除:删除头部元素,尾部元素填充头部元素,然后向下调整
  • 下述代码(插入、删除、向上调整、向下调整整体实现)
#include<iostream>
#include<vector>
#include<algorithm>class MaxHeap
{
private:std::vector<int> heap;//向上调整void AdjuestUp(int index){while (index > 0 && heap[index] > heap[(index - 1) / 2]){std::swap(heap[index], heap[(index - 1) / 2]);index = (index - 1) / 2;}}//向下调整void AdjuestDown(int index){int size = heap.size();while (index * 2 + 1 < size){int largest = index;int leftChild = index * 2 + 1;int rightChild = index * 2 + 2;//找到是三个节点的最大值if (leftChild < size && heap[leftChild]>heap[largest]){largest = leftChild;}if (rightChild<size && heap[rightChild]>heap[largest]){largest = rightChild;}//如果最大节点不是当前节点,则交换最大值,然后继续向下调整if (largest != index){std::swap(heap[index], heap[largest]);index = largest;}else{break;}}}

TopK问题

实现思路总结

  • 问题回顾:TopK就是从一组数据中找出前K大或者前K小的元素
  • 思路(通过小根堆解决
    • 维护一个大小为K的小根堆
    • 遍历全部的数据,将每个元素与栈顶元素(也就是最小值)比较
      • 如果当前元素大于栈顶元素,则替换掉栈顶元素,然后重新调整堆
      • 否则的话就继续遍历下一个元素
  • 事例(前K大的元素,因为小根堆的首元素永远都是最小的元素)

#include<iostream>
#include<queue>
#include<vector>using namespace std;vector<int> topKElements(vector<int>& data, int k)
{if (k <= 0) return {};//利用比较器实现小堆priority_queue<int, vector<int>, greater<int>> minheap;//遍历data中的全部属于,没添加满就继续添加,添加满了就比较堆顶元素for (int num : data){if (minheap.size() < k){minheap.push(num);}else if (num > minheap.top()){minheap.pop();minheap.push(num);}}vector<int>ret;while (!minheap.empty()){ret.push_back(minheap.top());minheap.pop();}return ret;
}int main() {vector<int> data = { 3, 1, 5, 12, 2, 11, 10, 7, 6 };int k = 3;vector<int> result = topKElements(data, k);cout << "Top " << k << " elements: ";for (int num : result) {cout << num << " ";}cout << endl;return 0;
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • JavaEE: 进程和线程
  • 《云数据中心网络架构与技术》第四章:金融数据中心网络的总体架构与方案演进
  • 面壁发布端侧视觉理解模型 MiniCPM-V 2.6;ChatGPT 人形机器人二代正式发布丨 RTE 开发者日报
  • RabbitMQ高级特性 - 消息分发(限流、负载均衡)
  • Leetcode 第 135 场双周赛题解
  • 深入JVM:类加载器和双亲委派模型
  • 如何搭建一个圈子社区系统?开源社交陪玩交友圈子论坛帖子系统保姆级搭建教程!
  • 益九未来CEO曾宪军:创新引领,打造智能售货机行业新标杆
  • vue项目路径使用@报错
  • VS Code C/C++ MSVC编译器
  • 【React 】react 创建项目配置 jsconfig.json 的作用
  • Axure RP界面设计初探:基础操作与实用技巧
  • JavaScript青少年简明教程:异常处理
  • Java 面试常见问题之——static 的用法
  • Android 在布局中tools使用
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • 2017前端实习生面试总结
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • ERLANG 网工修炼笔记 ---- UDP
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • JS数组方法汇总
  • Mithril.js 入门介绍
  • Python打包系统简单入门
  • ucore操作系统实验笔记 - 重新理解中断
  • webpack+react项目初体验——记录我的webpack环境配置
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 回顾 Swift 多平台移植进度 #2
  • 解析 Webpack中import、require、按需加载的执行过程
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 微信小程序设置上一页数据
  • 微信支付JSAPI,实测!终极方案
  • 新书推荐|Windows黑客编程技术详解
  • 一、python与pycharm的安装
  • NLPIR智能语义技术让大数据挖掘更简单
  • ​Python 3 新特性:类型注解
  • # centos7下FFmpeg环境部署记录
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (3)选择元素——(14)接触DOM元素(Accessing DOM elements)
  • (办公)springboot配置aop处理请求.
  • (超简单)构建高可用网络应用:使用Nginx进行负载均衡与健康检查
  • (动态规划)5. 最长回文子串 java解决
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (回溯) LeetCode 77. 组合
  • (亲测)设​置​m​y​e​c​l​i​p​s​e​打​开​默​认​工​作​空​间...
  • (原創) 如何安裝Linux版本的Quartus II? (SOC) (Quartus II) (Linux) (RedHat) (VirtualBox)
  • (原創) 如何解决make kernel时『clock skew detected』的warning? (OS) (Linux)
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (转)关于pipe()的详细解析
  • (转载)Google Chrome调试JS
  • ***详解账号泄露:全球约1亿用户已泄露
  • ./configure、make、make install 命令
  • .gitignore文件—git忽略文件
  • .jks文件(JAVA KeyStore)
  • .NET CORE使用Redis分布式锁续命(续期)问题