数据结构 二叉树和堆总结

概念

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

树的相关概念

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

二叉树

概念

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

二叉树性质

重要性质总结

  • 节点数与层数

    • 二叉树第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 树和红黑树。

二叉树存储结构

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

cpp 复制代码
#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节点是右子树(基本不用,只适合完全静态的树)

cpp 复制代码
#include <iostream>
#include <vector>
#include <cmath>

std::vector<int> createBinaryTreeArray() {
    // 假设二叉树的深度为3
    std::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;
}

前中后序遍历

总结

  • **前序遍历:**根--左--右
  • **后序遍历:**左--右--根
  • **中序遍历:**左--根--右
cpp 复制代码
#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;
}

层序遍历

实现方式:借助队列实现

  • 初始化队列,将根节点的数值放入进去
  • 设置循环直到队列为空: 队列中提取并出队,然后分别放入左右的数值进行循环判断
cpp 复制代码
#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

概念

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

向上调整与向下调整

向上调整

  • 插入新元素时,将新元素放入到堆的末尾
  • 从新插入元素的下标(堆的末尾),检查父节点是否比自己大或者小(根据大小堆来决定,大堆的话则是当前元素比父元素大就交换)
  • 如果大于或者小于父节点,则交换两个节点,然后当前下标更新成为父元素的下标,继续检查
  • 重复此过程,直到当前元素不大于父元素,或者已经到达栈顶
  • 事例代码(建大堆)
  • 场景:当插入新元素的时候使用向上调整,因为新元素是插入到数组尾部
cpp 复制代码
	//向上调整
	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;
		}
	}

向下调整

  • 从堆中删除第一个元素(大堆就是删除最大元素,小堆则是最小)下面叙述按照大堆逻辑。将堆的最后一个元素移动到堆顶
  • 从堆顶开始,找到当前节点、左孩子、右孩子三个节点的最大值
  • 如果当前节点不是最大值,则将其与最大值节点交换,并更新当前索引为最大子节点索引,继续检查
  • 重复上述过程,直到当前节点大于或者等于其子节点,或者已经到达堆的底部
  • 使用场景:删除元素的时候
  • 代码事例,大堆的向下调整
cpp 复制代码
//改写法适合排序堆排使用

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);
	}
}
cpp 复制代码
	//向下调整
	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)。

堆排序

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

  • 构建大堆,将数组转化成为堆
  • 交换堆顶元素与末尾元素,同时缩小堆的范围,对堆顶元素进行向下调整,直到数组排序完成
cpp 复制代码
#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 << " ";
	}
}

堆的简单实现

插入与删除实现

  • **插入:**末尾插入新元素,然后向上调整
  • **删除:**删除头部元素,尾部元素填充头部元素,然后向下调整
  • 下述代码(插入、删除、向上调整、向下调整整体实现)
cpp 复制代码
#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大的元素,因为小根堆的首元素永远都是最小的元素)
cpp 复制代码
#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;
}
相关推荐
hummhumm31 分钟前
第 10 章 - Go语言字符串操作
java·后端·python·sql·算法·golang·database
Jeffrey_oWang35 分钟前
软间隔支持向量机
算法·机器学习·支持向量机
算法歌者1 小时前
[算法]入门1.矩阵转置
算法
用户8134411823611 小时前
分布式训练
算法
林开落L2 小时前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色2 小时前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download2 小时前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna2 小时前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷2 小时前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿2 小时前
【算法】【优选算法】前缀和(上)
java·算法·前缀和