一.树的概念:
1.树的概念:
树是一种非线性的数据结构,它是由n个有限节点组成的一个具有层次关系的结合;
2.树的特点:
每个节点有0个或多个子节点;没有父节点的节点为根节点;每个非根节点有且只有一个父节点,每个子节点可以分为多个不相交的子树;
3.关于树的几个基本概念:
(1)节点的度:一个节点含有的子树的个数;
(2)叶节点:节点的度为0的节点;
(3)双亲节点:一个节点含有子节点,则该节点就为父节点;
(4)孩子结点:一个节点含有的子树的根节点称为该节点的子节点;
(5)节点的层次:从根开始为第一层,根的子节点为第二层,以此类推…;
(6)树的高度或深度:树中节点的最大层次;
(7)节点的祖先:从根到该节点所经分支上的所有节点;
二.二叉树的概念及结构:
1.概念:
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树 的二叉树组成;
2.特点:
(1)每个节点最多有两个子树,即二叉树不存在度大于二的节点;
(2)二叉树的子树有左右之分,其子树的次序不能颠倒;
3.性质:
(1)若规定根结点的层数为1,则一棵非空二叉树的第i层最多有2^(i-1)个结点;
(2)若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数为2^K-1个结点;
(3)对于任何一棵二叉树,如果其叶节点个数为n0,度为2的非叶子结点的个数为n2,则n0=n2+1;
(4)n个节点的完全二叉树的深度K为log2^(n+1)上取整;
(5)对于具有n个结点的完全二叉树,如果按照从上至下,从左至右的顺序对所有结点从0开始编号,则对于序号为i的结点有:
若i>0,双亲序号:(i-1)/2; i=0;i为根节点编号,无双亲结点;若2i+1<n,左孩子序号为:2i+1,否则无左孩子; 若2i+2<n,右孩子序号为:2i+2,否则无右孩子;
(6)子树是不相交的;除了根节点外每个节点仅仅有一个父节点; 一棵N个结点的树有N-1条边;
4.特殊二叉树:
(1)满二叉树:一个二叉树的每一层的节点都达到了最大值,则就是满二叉树;如果该二叉树的层数为K层,则二叉树的节点个数为2^K -1;
(2)完全二叉树:一棵二叉树至多只有最下面的一层上的结点的度数可以小于2,并且最下层上的结点都集中在该层最左边的若干位置上,而在最后一层上,右边的若干结点缺失的二叉树,则此二叉树成为完全二叉树;
5.二叉树的存储结构:
(1)顺序存储:顺序结构的存储就是通过数组来进行存储;完全二叉树用顺序存储来进行存储,因为不会造成空间上的浪费;堆的创建和算法会通过顺序存储来进行存储;
(2)链式存储:二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链表来指示元素的逻辑关系;通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址;
三.二叉树的顺序结构及实现:
普通的二叉树不适合使用顺序结构进行存储,因为会造成大量的空间浪费,而完全二叉树可以使用顺序结构来进行存储;而二叉树的顺序存储主要是来进行堆存储;
1.堆的概念及结构:
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆);
如下图所示:
根结点最大的堆为大堆,根结点最小的堆为小堆;
2.堆的实现:向下调整法
(从上往下进行二叉树的结点的调整,使其满足最大堆或最小堆)
(1)前提条件:除了一个位置外,其他地方都满足堆的性质;
- 找到根,左孩子或右孩子中最小的一个放在根上(只要有叶子一定有左孩子,不一定有右孩子);
- 停止条件,走到叶子的位置;
(2)相关公式:
根的结点:rootldx;
左孩子的结点:leftIdx=2*rootIdx+1;
右孩子的结点:rightIdx=2*rootIdx+2;
已知孩子的结点,求根的结点:rootIdx=(size-1-1)/2;
#include <stdio.h>
#include <stdlib.h>
void ADjustdown(int arr[],int size,int Idxroot){
int Idxleft=2*Idxroot+1;
int Idxright=2*Idxroot+2;
if(Idxleft>=size){ //判断Idxleft是否有叶子;如果没有叶子直接返回;
return ;
}
//如果有叶子,找到最小的值;
int Idxmid=Idxleft;
if(Idxright<size && arr[Idxright]<arr[Idxleft]){
Idxmid=Idxright;
}
//最小孩子的下标是Idxmid;
if(arr[Idxroot]<arr[Idxmid]){
//最小的堆就是Idxroot,满足堆的性质;
return;
}
else{
int tmp=arr[Idxroot];
arr[Idxroot]=arr[Idxmid]
arr[Idxmid]=tmp;
//如果交换则会破坏下面的堆的结构,因此需要向下调整;
return Adjustdowm(arr,size,Idxmid);
}
}
3.创建堆:把一个随机分布的无序数列变成一个满足堆的性质;
倒着调整;最后一个非叶子结点就是最后一个结点的双亲,即:
parent=(child-1)/2;-----》parent=(size-1-1)/2;
#include <stdio.h>
#include <stdlib.h>
void ADjustdown(int arr[],int size,int Idxroot){
int Idxleft=2*Idxroot+1;
int Idxright=2*Idxroot+2;
if(Idxleft>=size){ //判断Idxleft是否有叶子;如果没有叶子直接返回;
return ;
}
//如果有叶子,找到最小的值;
int Idxmid=Idxleft;
if(Idxright<size && arr[Idxright]<arr[Idxleft]){
Idxmid=Idxright;
}
//最小孩子的下标是Idxmid;
if(arr[Idxroot]<arr[Idxmid]){
//最小的堆就是Idxroot,满足堆的性质;
return;
}
else{
int tmp=arr[Idxroot];
arr[Idxroot]=arr[Idxmid]
arr[Idxmid]=tmp;
//如果交换则会破坏下面的堆的结构,因此需要向下调整;
return Adjustdowm(arr,size,Idxmid);
}
}
void CreatHeap(int arr[],int size){
for(int i=(size-2)/2;i>0;--i){
ADjustdown(arr,size,i);
}
}
利用堆的找最值的特点做具体的应用:
1.优先级对列;
2.排序问题;
3.Top K问题;
关于堆的排序问题:
排升序建大堆,排降序建小堆;
#include<iostream>
using namespace std;
void AdjustDown(int[] , int , int );
void BuildMaxHeap(int A[], int len)
{
for (int i = len / 2; i >= 1; i--)
//从完全二叉树的最后一个非叶子节点开始调整建立大根堆
{
AdjustDown(A, i, len);
}
}
void AdjustDown(int A[], int k, int len)
{
int temp = A[k];//保存当前结点的值
for (int i = 2 * k; i <= len; i = i * 2) //从其右孩子开始
{
if (i < len && A[i] < A[i + 1]) //如果有两个孩子,找到两个孩子中最大孩子的下标
i++;
if (temp >= A[i])
break; //已经是该结点大于其两孩子结点,不用再继续比较了
else
{
A[k] = A[i]; //将大的向上调整
k = i; //移到当前结点的最大孩子结点处作为新的起始结点,再继续向下比较
}
}
A[k] = temp;//找到了最终的位置并放入
}
void HeapSort(int A[], int len)
//堆排序:每次将堆顶元素和最后一个元素交换,再将剩余i-1个元素调整为新的堆,重复到堆只有一个元素
{
BuildMaxHeap(A, len);
for (int i = len; i >= 1; i--)//从堆的最后一个元素开始交换
{
swap(A[i], A[1]); //每次堆顶元素和堆的最后一个元素交换
AdjustDown(A, 1, i - 1);//将剩余i-1个元素调整为新的堆
}
}
int main()
{
int A[9] = {0,53,17,78,9,45,65,87,32};
//为了方便下标为i时左右孩子的下标为2i,2i+1,数组从1开始存储
HeapSort(A, 8);
for (int i = 1; i <= 8; i++){
cout << A[i] << endl;
}
system("pause");
return 0;
}