数据结构之二叉树

一.树的定义

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。

1.有一个特殊的结点,称为根结点,根结点没有前驱结点。
2.除根结点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、......、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继节点。
3.因此,树是递归定义的。

注意:树形结构中,子树之间不能有交集,否则就不是树形结构。

树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法
等。

二.树的相关概念

  • 深度:从根节点到最远叶子节点的最长路径上的节点数。

  • 高度:从当前节点到叶子节点的最长路径上的节点数。

  • :节点的子节点数, 如上图:A的为 6 (二叉树中节点的度不超过2)。

  • 叶子节点 :没有子节点的节点 (度为0的结点)。

  • 内部节点:至少有一个子节点的节点。

  • 父节点:若一个结点含有子节点,则这个结点称为其子节点的父节点。

  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。


三.二叉树的概念及结构

一棵二叉树是结点的一个有限集合,该集合:

  1. 或者为空
  2. 由一个 根结点加上两棵别称为左子树和右子树 的二叉树组成
  3. 二叉树不存在度大于2的结点
  4. 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

3.1.特殊的二叉树

  • 完全二叉树 :判断二叉树是否是完全二叉树(所有层都填满,除了最后一层,且最后一层从左到右填充 )。完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

  • 满二叉树 :判断二叉树是否是满二叉树(每个节点都有0或2个子节点)。也就是说,如果一个二叉树的层数为K,且结点总数是(2的k次方-1)则它就是满二叉树。

  • 二叉搜索树(BST):判断二叉树是否是二叉搜索树(左子树所有节点小于根节点,右子树所有节点大于根节点)。(后续文章会说明)

3.2.二叉树的性质

  1. 若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2的i-1次方个节点。

  2. 若规定根结点的层数为1,则深度为h的二叉树的最大结点数是2的h次方-1个节点。

3.对任何一棵二叉树, 如果度为0其叶结点个数为m,度为2的分支结点个数为n,则有m=n+1。

3.3.二叉树的存储结构

3.3.1.顺序结构

顺序结构存储就是使用数组来存储 ,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有 才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

堆是一种完全二叉树,可以用数组实现,分为最大堆和最小堆。主要操作是插入和删除,时间复杂度为O(log n),构建堆的时间复杂度是O(n)。
现实中我们通常把堆使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。
堆的性质
堆中某个结点的值总是不大于或不小于其父结点的值。堆总是一棵完全二叉树。

堆的实现

Heap.h

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>


typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);

void HPInit(HP* php);
void HPDestroy(HP* php);
void HPPush(HP* php, HPDataType x);
void HPPop(HP* php);
HPDataType HPTop(HP* php);
bool HPEmpty(HP* php);

Heap.c

cpp 复制代码
#include"Heap.h"

void HPInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;

}
void HPDestroy(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}
void Swap(HPDataType* pa, HPDataType* pb)
{
	HPDataType tmp = *pa;
	*pa = *pb;
	*pb = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
	//计算父亲数组下标 满足这种关系
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			//孩子成为新的父亲
			child = parent;
			//计算新父亲数组下标
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}

}
void HPPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newcapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail!");
			return;
		}
		php->a = tmp;
		php->capacity = newcapacity;
	}
	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

void AdjustDown(HPDataType* a, int n, int parent)
{
	int child = parent * 2 + 1;
	while (child<n)
	{
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
//要求删除根(堆顶)位置的数据 
void HPPop(HP* php)
{
	assert(php);
	assert(php->size > 0);
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size,0);
}
HPDataType HPTop(HP* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

bool HPEmpty(HP* php)
{
	assert(php);
	return php->size==0;

}
建堆之向上调整算法和向下调整算法
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
#include <time.h>
void test01()
{
	int a[] = { 4,2,8,1,5,6,9,7};
	HP hp;
	HPInit(&hp);
	int sz =  sizeof(a) / sizeof(int);
	for (size_t i = 0; i < sz; i++)
	{
		HPPush(&hp, a[i]); 

	}
	int i = 0;
	while (!HPEmpty(&hp))
	{
		a[i++] = HPTop(&hp);
		HPPop(&hp);
	}
	for (i = 0; i < sz; i++)
	{
		printf("%d ",a[i]);
	}

}
int main()
 {
	test01();
	return 0;
}

运行结果为:

我们也就可以衍生为TopK问题

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
#include <time.h>
void test01()
{
	int a[] = { 4,2,8,1,5,6,9,7};
	HP hp;
	HPInit(&hp);
	int sz =  sizeof(a) / sizeof(int);
	for (size_t i = 0; i < sz; i++)
	{
		HPPush(&hp, a[i]); 
	}

	int k = 0;
	scanf("%d", &k);
	//找出最大的前k个
	while (k--)
	{
		printf("%d ", HPTop(&hp));
		HPPop(&hp);
	}
	HPDestroy(&hp);
}

我们也就可以将向上调整算法与向下调整算法结合 进行堆排序

(1)向上调整建堆 排序

cpp 复制代码
//堆排序 O(N*logN)
void HeapSort(int* a, int n)
{
	//想要得到的最终数据为降序 建小堆
	//想要得到的最终数据为升序 建大堆
	//向上调整建堆 时间复杂度O(N*logN)
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

	int end = n - 1;

	//O(N*log N)
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

(2)向下调整建堆

cpp 复制代码
void HeapSort(int* a, int n)
{

	//向下调整建堆 时间复杂度O(N) 更好
	//从最后一个数的父亲开始调整
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	int end = n - 1;

	//O(N*log N)
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

大量数据的Topk问题

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
#include <time.h>
void GreateNData()
{
	//造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen errpr");
		return;
	}
	for (int i = 0; i < n; ++i)
	{
		int x = (rand()+i)%10000000;
		fprintf(fin, "%d\n", x);
	}
	fclose(fin);
}

//top k问题
//思路:用前k个数,建一个小堆,剩下的数据与堆顶数据比较
//如果比堆顶的数据大,就替代堆顶进堆 (覆盖根位置,然后向下调整)
//最后 这个小堆中的k个,就是最大的前 k 个
void HeapTest03()
{
	int k = 0;
	printf("请输入k:");
	scanf("%d", &k);
	int* arr = (int*)malloc(sizeof(int) * k);
	if (arr == NULL)
	{
		perror("malloc fail!");
		return;
	}
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}
	//读取文件的前k个
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &arr[i]);
	}
	//建小堆
	for(int i = (k - 1 - 1) / 2;i >= 0; i--)
	{
		AdjustDown(arr, k, i);
	}
	//读取剩下的N-K个数据
	int x = 0;
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > arr[0])
		{
			arr[0] = x;
			AdjustDown(arr, k, 0);
		}
	}

	printf("最大的前%d个数:", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", arr[i]);
	}
}
int main()
 {
	
	GreateNData();
	HeapTest03();
	return 0;
}

3.3.2.链式结构

cpp 复制代码
typedef int BTDataType;
typedef struct BinaryTreeNode
{
 BTDataType _data;
 struct BinaryTreeNode* _left;
 struct BinaryTreeNode* _right;
}BTNode;
二叉树的遍历

二叉树遍历主要有三种基本方法:前序遍历、中序遍历和后序遍历 。这三种都属于深度优先搜索(DFS)的方式。另外还有层序遍历,也就是广度优先搜索(BFS),按层次遍历节点。

1.前序遍历 根节点 → 左子树 → 右子树

前序遍历的顺序是根节点、左子树、右子树。也就是说,先访问根节点,然后递归地前序遍历左子树,接着递归地前序遍历右子树。

假设空节点为N

cpp 复制代码
void PrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}

递归展开图

前序遍历结果:1 2 3 4 5 6 。

2.中序遍历左子树 → 根节点 → 右子树
cpp 复制代码
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}

	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}


中序遍历结果: 3 2 1 5 4 6。

3.后序遍历左子树 → 右子树 → 根节点

同理可得

cpp 复制代码
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	LeftOrder(root->left);
	LeftOrder(root->right);
	printf("%d ", root->data);
}
4.层序遍历

(用c++写会更方便)

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

using namespace std;

// 二叉树节点定义
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

vector<vector<int>> levelOrder(TreeNode* root) {
    vector<vector<int>> result;
    if (!root) return result;
    
    queue<TreeNode*> q;
    q.push(root);
    
    while (!q.empty()) {
        int levelSize = q.size();
        vector<int> currentLevel;
        
        for (int i = 0; i < levelSize; ++i) {
            TreeNode* node = q.front();
            q.pop();
            currentLevel.push_back(node->val);
            
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
        
        result.push_back(currentLevel);
    }
    
    return result;
}

// 测试代码
int main() 
{

    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->right = new TreeNode(6);
    
    vector<vector<int>> traversal = levelOrder(root);
    
    cout << "层序遍历结果:" << endl;
    for (auto& level : traversal) {
        cout << "[";
        for (size_t i = 0; i < level.size(); ++i) {
            cout << level[i];
            if (i < level.size()-1) cout << ", ";
        }
        cout << "]" << endl;
    }
    
    delete root->left->left;
    delete root->left->right;
    delete root->right->right;
    delete root->left;
    delete root->right;
    delete root;
    
    return 0;
}
二叉树其他字段
1.TreeSize(二叉树结点个数)
cpp 复制代码
int TreeSize(BTNode* root)
{
	return root == NULL ? 0 :
		TreeSize(root->left) + TreeSize(root->right) + 1;
}
2.TreeLeafSize(二叉树叶子结点个数)
cpp 复制代码
int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;

	if (root->left == NULL && root->right == NULL)
		return 1;

	return TreeLeafSize(root->left)
		+ TreeLeafSize(root->right);
}
3.TreeLevelKSize (二叉树第k层结点个数
cpp 复制代码
int TreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;

	if (k == 1)
		return 1;

	// 子问题
	return TreeLevelKSize(root->left, k - 1)
		+ TreeLevelKSize(root->right, k - 1);
}
4.TreeFind(二叉树查找值为x的结点)
cpp 复制代码
// 二叉树查找值为x的节点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;

	if (root->data == x)
		return root;

	BTNode* ret1 = TreeFind(root->left, x);
	if (ret1)
		return ret1;

	BTNode* ret2 = TreeFind(root->right, x);
	if (ret2)
		return ret2;

	return NULL;
}
5.TreeHeight(子树高度)
cpp 复制代码
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ?
		leftHeight + 1 : rightHeight + 1;
}
6.BinaryTreeDestory(二叉树的销毁)
cpp 复制代码
void TreeDestory(BTNode* root)
{
	if (root == NULL)
		return;

	TreeDestory(root->left);
	TreeDestory(root->right);
	free(root);
}
7.判断二叉树是否是满二叉树
cpp 复制代码
// 判断是否为满二叉树
bool isFullBinaryTree(TreeNode* root) {
    if (root == nullptr)
        return true;
    if (root->left == nullptr && root->right == nullptr)
        return true;

    if (root->left != nullptr && root->right != nullptr) 
    {
        return isFullBinaryTree(root->left) && isFullBinaryTree(root->right);
    }

    return false;
}
8.判断二叉树是否为完全二叉树
cpp 复制代码
// 判断是否为完全二叉树
bool isCompleteBinaryTree(TreeNode* root) 
{
    if (root == nullptr) 
        return true; // 空树是完全二叉树
    queue<TreeNode*> q;
    q.push(root);
    bool foundNull = false; // 标记是否遇到空节点
    while (!q.empty()) 
    {
        TreeNode* current = q.front();
        q.pop();

        // 检查当前节点
        if (current == nullptr) 
        {
            foundNull = true; // 遇到空节点
        }
        else 
        {
            // 如果已经遇到空节点,又发现非空节点,则不是完全二叉树
            if (foundNull) 
            {
                return false;
            }
            // 将左右子节点入队(即使是空节点也入队)
            q.push(current->left);
            q.push(current->right);
        }
    }

    return true; // 所有节点都满足条件
}
二叉树OJ题目
1.单值二叉树

965. 单值二叉树 - 力扣(LeetCode)

cpp 复制代码
bool isUnivalTree(struct TreeNode* root) {
    if(root==NULL)
        return true;
    if(root->left&&root->left->val!=root->val)
        return false;
    if(root->right&&root->right->val!=root->val)
        return false;
    return isUnivalTree(root->left)&&isUnivalTree(root->right);
}
2.相同的树

100. 相同的树 - 力扣(LeetCode)

cpp 复制代码
bool isSameTree(struct TreeNode* p, struct TreeNode* q) 
{
    if(p == NULL && q == NULL)
        return true;
    if(p == NULL||q ==NULL)
       return false;
    if(p->val!=q->val)
      return false;

    return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}
3.对称二叉树

101. 对称二叉树 - 力扣(LeetCode)

cpp 复制代码
bool _isSymmetric(struct TreeNode* p,struct TreeNode* q)
{
    if(p == NULL && q ==NULL)
        return true;
    //其中有一个为空 则为false
    if(p == NULL || q == NULL)
        return false;
    if(p->val!=q->val)
        return false;
    if(p->left == q->right && p->right == q->left)
        return true;

    return _isSymmetric(p->left,q->right)&&_isSymmetric(p->right,q->left);

}
bool isSymmetric(struct TreeNode* root) {
    return _isSymmetric(root->left,root->right);
}
4.二叉树的前序遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)

cpp 复制代码
 //前端遍历后的值放入数组中
int Treesize(struct TreeNode* root)
{
    return root == NULL ? 0 :Treesize(root->left) + Treesize(root->right) + 1;
}
void BinaryTreePrevOrder(struct TreeNode* root, int* arr, int* pi)
{
    if (root == NULL)
        return;
    arr[(*pi)++] = root->val;
    BinaryTreePrevOrder(root->left, arr, pi);
    BinaryTreePrevOrder(root->right, arr, pi);
}
int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize = Treesize(root);
    int* arr = (int*)malloc(sizeof(int) * (*returnSize));
    int i = 0;
    BinaryTreePrevOrder(root, arr, &i);
    return arr;
}
5. 二叉树的中序遍历

94. 二叉树的中序遍历 - 力扣(LeetCode)

cpp 复制代码
void BinaryTreeMiddleOrder(struct TreeNode* root,int* arr,int* pi)
{
    if (root == NULL)
        return;
    BinaryTreeMiddleOrder(root->left, arr, pi);
    arr[(*pi)++] = root->val;
    BinaryTreeMiddleOrder(root->right, arr, pi);
}
int TreeSize(struct TreeNode* root)
{
    if(root == NULL)
    return 0;
    return TreeSize(root->left) + TreeSize (root->right) +1;
}
int* inorderTraversal(struct TreeNode* root, int* returnSize) {
    *returnSize = TreeSize(root);
    int* array = (int*)malloc(sizeof(int)*(*returnSize));
    int i =0;
    BinaryTreeMiddleOrder(root,array,&i);
    return array;
}
6.二叉树的后序遍历

同理4.5。

145. 二叉树的后序遍历 - 力扣(LeetCode)

7.另一颗树的子树

572. 另一棵树的子树 - 力扣(LeetCode)

cpp 复制代码
bool isSameTree(struct TreeNode* p, struct TreeNode* q) 
{
    if(p==NULL&&q==NULL)
       return true;
    if(p==NULL||q==NULL)
       return false;
    if(p->val!=q->val)
      return false;

    return  isSameTree(p->left,q->left)
    &&isSameTree(p->right,q->right);
}
//找出root中所有的子树和Subroot进行比较
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
    //subRoot 不可能为空
    if(root==NULL)
       return false;
    if(root->val==subRoot->val&&isSameTree(root,subRoot))
       return true;
    
    return isSubtree(root->left,subRoot) || isSubtree(root->right,subRoot);
}
8 .二叉树的构建及遍历

二叉树遍历_牛客题霸_牛客网

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct BinaryTreeNode {
    char val;
    struct BinaryTreeNode* left;
    struct BinaryTreeNode* right;
} BTNode;
BTNode* CreatBinaryTree(char* a,int* pi)
{
    if(a[*pi]=='#')
    {
        (*pi)++;
        return NULL;
    }
    BTNode* root=(BTNode*)malloc(sizeof(BTNode));
    root->val=a[(*pi)++];
    root->left=CreatBinaryTree(a,pi);
    root->right=CreatBinaryTree(a,pi);

    return root;

}
//中序遍历
void BinaryTreeInOrder(BTNode* root) {
    if (root == NULL) {
        return;
    }
    BinaryTreeInOrder(root->left);
    printf("%c ", root->val);
    BinaryTreeInOrder(root->right);
}
int main()
{
    char arr[100];
    scanf("%s",arr);
    int i=0;
    BTNode* root=CreatBinaryTree(arr,&i);
    BinaryTreeInOrder(root);
    return 0;
}
相关推荐
许小燚2 小时前
线性表——双向链表
数据结构·链表
董董灿是个攻城狮2 小时前
5分钟搞懂什么是窗口注意力?
算法
Dann Hiroaki2 小时前
笔记分享: 哈尔滨工业大学CS31002编译原理——02. 语法分析
笔记·算法
jz_ddk3 小时前
[学习] C语言数学库函数背后的故事:`double erf(double x)`
c语言·开发语言·学习
xiaolang_8616_wjl3 小时前
c++文字游戏_闯关打怪2.0(开源)
开发语言·c++·开源
夜月yeyue3 小时前
设计模式分析
linux·c++·stm32·单片机·嵌入式硬件
qqxhb4 小时前
零基础数据结构与算法——第四章:基础算法-排序(上)
java·数据结构·算法·冒泡·插入·选择
无小道4 小时前
c++-引用(包括完美转发,移动构造,万能引用)
c语言·开发语言·汇编·c++
晚云与城4 小时前
【数据结构】顺序表和链表
数据结构·链表