树
概念
树是一种层次结构非线性的数据结构,其是由节点和边组成,可以用来表示层次关系的数据。
树的相关概念
- **节点:**树的基本组成单位,每个节点都包含数据,同时与其他节点相互连接
- 根节点:树的顶层节点,没有父节点
- **子节点:**由一个节点直接连接的下层节点
- **父节点:**直接连接子节点的上层节点
- **叶子节点:**没有子节点的就是叶子节点
- **内部节点:**至少有一个子节点的节点
- **兄弟节点:**具有同一个父节点的节点
- **路径:**从一个节点到另外一个节点所经过的节点路径
- **深度:**节点到根节点的路径长度
- **高度:**节点到叶子节点的最长路径长度
- **度:**节点的子节点数
- **子树:**由某个节点机器所有后代节点组成的树
二叉树
概念
二叉树是一种特殊的树结构,其每个节点最多有两个子节点,也就是左子节点和右子节点,二叉树可以为空(没有节点,也可以只有一个根节点)。
二叉树性质
重要性质总结
节点数与层数:
- 二叉树第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(nlogn)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(nlogn)=O(nlogn)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;
}