数据结构之树知识总结
树型结构:
1、树的基本概念
一种表示层次关系(一对多)的数据节构
有且只有一个特定的节点,该节点没有前趋节点,被称为根节点
剩余的n个互不相交的子集,其中每个子集也都是一棵树,都被称为根节点的子树
注意:树型节构具有递归性(树中有树)
2、树的表示方式:
倒悬树、嵌套法、凹凸法
3、树的专业术语
节点:组成树的基础元素,同时它也是一棵树
节点的度:该节点子树的数量
树的度:树中所有节点的度的最大值
树的密度:树中所有节点的数量
树的深度(高度):树的最大层次为树的深度
节点的层次:根节点的层次为1,它的孩子层次为2,孩子的孩子层次为3,以此类推
叶子节点:节点的度为0的节点
双亲节点和孩子节点:节点的子树被称为该节点的孩子节点,该节点就是孩子节点的双亲节点
兄弟节点:具有同一个双亲节点,互为兄弟节点
堂兄弟:双亲节点互为兄弟节点
祖先:从根节点出发到该节点,路径上经过的所有节点都称为该节点的祖先
子孙:一个节点的子树中任意一个节点都是它的子孙
4、树的存储
树可以顺序存储、链式存储,还可以混合存储
可以根据存储的信息不同,树有以下存储方式:
双亲表示法: 顺序位置 data 双亲下标0 A -11 B 02 C 03 D 14 E 15 F 16 G 27 H 4 8 X 0 优点:方便找到双亲缺点:查找孩子节点麻烦孩子表示法:顺序: 浪费内存位置 data son_arr(存储子节点的数组)0 A 1,2,81 B 3,4,52 C 63 D 4 E 7 5 F 6 G 7 H 8 X 链式: 节约内存空间位置 data ListHead(存储子节点的链表)0 A 1->2->8->N1 B 3->4->5->N2 C 6->N3 D N 4 E 7->N 5 F N6 G N7 H N8 X N优点:查找孩子节点方便 缺点:找双亲不方便兄弟表示法:链式双亲只存储第一个子节点 数据 链式指向所有兄弟节点优点:可以方便找到所有的兄弟节点缺点:找双亲麻烦注意:普通树不常用,一般会使用二叉树进行存储
树
#include <stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include"link_array_queue.h"
typedef struct TreeNode
{ char data; int parent;
}TreeNode;
typedef struct Tree
{ TreeNode *arr; int cnt; int cap;
}Tree;
Tree *create_tree(int cap)
{ Tree *tree=malloc(sizeof(Tree)); tree->arr=malloc(sizeof(TreeNode)*cap); tree->cap=cap; tree->cnt=0; return tree;
}
bool add_tree_node(Tree *tree,char data,int pdata)
{ if('\0'==pdata && tree->cnt==0) { tree->arr[0].data=data; tree->arr[0].parent=-1; tree->cnt++; return true; } for(int i=0;i<tree->cnt;i++) { if(tree->arr[i].data==pdata) { tree->arr[tree->cnt].data=data; tree->arr[tree->cnt++].parent=i; return true; } }
}
void show_tree(Tree *tree)
{ for(int i=0;i<tree->cnt;i++) { printf("index:%d data:%c parent:%d\n",i,tree->arr[i].data,tree->arr[i].parent); }
}
int _degree(Tree *tree,char data)
{ int degree=0; int index=-1; for(int i=0;i<tree->cnt;i++) { if(tree->arr[i].data==data) index=i; } if(index==-1)return -1; for(int i=0;i<tree->cnt;i++) { if(tree->arr[i].parent=index) degree++; } return degree;
}
// 求树的高度
int _tree_height(Tree *tree,char data)
{ int index = 0; for (int i = 0; i < tree->cnt; i++) { if(tree->arr[i].data==data) { index=i; break; } }
int height=0; for (int j = 0; j < tree->cnt; j++) { if (tree->arr[j].parent == index) { int high=_tree_height(tree,tree->arr[j].data); if (high > height) { height = high; } } } return height+1;
}
//求数高度
int tree_height(Tree *tree)
{
return _tree_height(tree,tree->arr[0].data);
}
//层次遍历
void layer_show_tree(Tree *tree)
{ ListQueue *queue=create_list_queue(); push_list_queue(queue,tree->arr[0].data); while(!empty_list_queue(queue)) { //出队一个结点并显示 char data=head_list_queue(queue); pop_list_queue(queue); printf("%c\n",data); //找出结点的子结点入队 int index;//先找到节点的下标 for(int i=0;i<tree->cnt;i++) { if(data==tree->arr[i].data) { index=i; break; } } for(int i=0;i<tree->cnt;i++) { if(index==tree->arr[i].parent) { push_list_queue(queue,tree->arr[i].data); } } } destroy_list_queue(queue);
}
int main(int argc, const char* argv[])
{ Tree *tree = create_tree(10); add_tree_node(tree, 'A', '\0'); // 添加根节点 add_tree_node(tree, 'B', 'A'); // 添加子节点B,父节点为A add_tree_node(tree, 'C', 'A'); // 添加子节点C,父节点为A add_tree_node(tree, 'D', 'B'); // 添加子节点D,父节点为B add_tree_node(tree, 'E', 'B'); // 添加子节点E,父节点为B add_tree_node(tree, 'F', 'C'); // 添加子节点F,父节点为C show_tree(tree); layer_show_tree(tree); printf("Degree of node B: %d\n", _degree(tree, 'B')); // 测试计算节点B的度数 printf("Degree of node A: %d\n", _degree(tree, 'A')); // 测试计算节点A的度数 printf("Height of tree: %d\n", tree_height(tree)); // 测试计算树的高度 return 0;
}
link_array_queue.h
#ifndef QUEUE_H
#define QUEUE_H#include <stdio.h>
#include<stdlib.h>
#include<stdbool.h>#define TYPE int//节点
typedef struct Node
{TYPE data;struct Node *next;
}Node;//创建节点
Node *create_Node(TYPE val);//链队列
typedef struct ListQueue
{Node *front;Node *rear;size_t cnt;}ListQueue;//创建链队列
ListQueue *create_list_queue(void);
//空队列
bool empty_list_queue(ListQueue *queue);//入队
void push_list_queue(ListQueue *queue,TYPE val);//出队
bool pop_list_queue(ListQueue *queue);//队头
TYPE head_list_queue(ListQueue *queue);
//队尾
TYPE tail_list_queue(ListQueue *queue);//队列元素数量
size_t size_list_queue(ListQueue *queue);
//销毁
void destroy_list_queue(ListQueue *queue);#endif //QUEUE_H
二叉树:
是一种常用的数据结构,比普通树处理起来要简单,而且普通树也比较方便地转换成二叉树
-
定义:节点的度最多为2
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个节点
2.特殊的二叉树类型:
满二叉树:每层的节点数都是2^(i-1)
完全二叉树:深度为k,有n个节点的二叉树当且仅当其每一个节点都与深度为k的满二叉树中编号从1到n的节点一一对应时,称为完全二叉树
3.二叉树的重要性质:(重点)
性质1:二叉树的第i层上至多有2^(i-1)(i≥1)个节点
性质2:深度为h的二叉树中至多含有2^h-1个节点
性质3:若在任意一棵二叉树中,有n0个叶子节点,有n2个度为2的节点,则必有n0=n2+1
性质4:具有n个节点的满二叉树深为log2n+1
性质5:若对一棵有n个节点的完全二叉树进行顺序编号(1≤i≤n),那么,对于编号为i(i≥1)的节点:
当i=1时,该节点为根,它无双亲节点
当i>1时,该节点的双亲节点的编号为i/2。
若2i≤n,则有编号为2i的左节点,否则没有左节点
若2i+1≤n,则有编号为2i+1的右节点,否则没有右节点
练习题:
1.已知一棵二叉树中,有20个叶子结点,其中10个结点只有
左孩子,15个结点只有右孩子,求该二叉树的总结点数。
2.求100个结点的完全二叉树的叶子结点数
3.完全二叉树的第7层有10个叶子结点,总共有多少个叶子结点
4.判断题:完全二叉树最多有1个度为1的结点
二叉树的操作:
构建、销毁、遍历、高度、密度、插入、删除、查询、求左、求右、求根二叉树的存储:顺序存储:必须按照完全二叉树的格式,把每个节点按照从上到下、从左到右的顺序依次存入连续的内存中,如果有空位置则使用特殊数据代替存入数据项:存储节点的内存首地址容量链式存储:由一个个链式节点组成,每个节点就是一棵树节点数据项:数据左子树指针右子树指针
二叉树的遍历(重点)前序:根、左、右中序:左、根、右后序:左、右、根层序:从上到下、从左到右依次遍历一棵树注意:前中后由根节点决定,并且左右子树的遍历顺序不会改变注意:根据 前序+中序 或者 后序+中序 就可以还原一棵树,只有 前序+后序 是无法还原的注意:层序遍历必须配合队列进行
实现二叉树:
顺序表实现二叉树:http://t.csdnimg.cn/dFnfB
链表实现二叉树:http://t.csdnimg.cn/AHxLJ
有序二叉树
左子树的数据小于根,右子树的数据大于等于根,这种树称为有序二叉树、二叉搜索树、二叉排序树
注意:这种树的节点需要频繁地插入、删除,因此不适合顺序存储
注意:插入、删除都要保证有序
有序二叉树的中序遍历刚好就是从小到大,所以有序二叉树也是一种排序算法,查找又天然是二分查找,所以经常考
有序二叉树实现:http://t.csdnimg.cn/40LyH
二叉树练习题
1、把一棵二叉树转换为它的镜像树。
2、输入两棵二叉树A,B,判断B是不是A的子结构(我们约定空树不是任意一个树的子结构)。
3、将一棵有序二叉树转换成一个有序的双向链表。
4、计算出有序二叉树中倒数第K个大的数。
5、判断一个二叉树是否对称。
6、请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。
解题思路:
//镜像树
void mirror_tree(TreeNode *root)
{if(root==NULL)return;TreeNode *temp=root->left;root->left=root->right;root->right=temp;mirror_tree(root->left);mirror_tree(root->right);
}//输入两棵二叉树A,B,判断B是不是A的子结构(我们约定空树不是任意一个树的子结构)。
bool is_same_tree(TreeNode* A, TreeNode* B) {if (B == NULL) return true; if (A == NULL) return false;if (A->data != B->data) return false; // 数据不相等,不是子结构return is_same_tree(A->left, B->left) && is_same_tree(A->right, B->right);
}bool is_child_tree(TreeNode* parent, TreeNode* child) {if (parent == NULL || child == NULL) {return false; // 空树不是任意一个树的子结构}if (is_same_tree(parent, child)) {return true; // 完全匹配,是子结构}return is_child_tree(parent->left, child) || is_child_tree(parent->right, child);
}//将二叉树转换成链表//尾添加链表
void _add_tail_list(TreeNode **head,TreeNode *node)
{if(NULL==*head)*head=node;else{(*head)->left->right=node;node->left=(*head)->left;}(*head)->left=node;
}//
void _tree_to_list(TreeNode *root,TreeNode **head)
{if(NULL==root)return;//按照中序添加到链表_tree_to_list(root->left,head);_add_tail_list(head,root);_tree_to_list(root->right,head);
}//
TreeNode *tree_to_list(TreeNode *root)
{TreeNode *head=NULL;_tree_to_list(root,&head);head->left->right=head;return head;
}//判断一个二叉树是否对称。
bool check(TreeNode* node1, TreeNode* node2)
{if(!node1 && !node2) return true;if((!node1 && node2) || (node1 && !node2)) return false;//都不为空,但是val不同if(node1->data != node2->data) return false;//都不为空,但是val相同,还要看这两棵树是否是镜像的,要分别看外侧和内侧bool outside = check(node1->left, node2->right);bool inside = check(node1->right, node2->left);return outside && inside;}bool isSymmetric(TreeNode* root) {if(!root)return true;return check(root->left, root->right);}//请实现一个函数按照之字形打印二叉树
void printf_zhi(TreeNode *root)
{if (NULL == root) return;ListStack *stack1 = create_Link_stack();ListStack *stack2 = create_Link_stack();push_Link_stack(stack1, root->data); // 将根节点入栈bool left_to_right = true; // 标记从左往右打印while (!empty_list_stack(stack1) || !empty_list_stack(stack2)){ListStack *current_stack = left_to_right ? stack1 : stack2;ListStack *next_stack = left_to_right ? stack2 : stack1;while (!empty_list_stack(current_stack)){TREE_TYPE data = top_list_stack(current_stack);printf("%c ", data);pop_List_stack(current_stack);TreeNode *current_node = find_node(root, data); // 找到当前节点if (current_node != NULL){if (left_to_right){if (current_node->left != NULL) push_Link_stack(next_stack, current_node->left->data);if (current_node->right != NULL) push_Link_stack(next_stack, current_node->right->data);}else{if (current_node->right != NULL) push_Link_stack(next_stack, current_node->right->data);if (current_node->left != NULL) push_Link_stack(next_stack, current_node->left->data);}}}left_to_right = !left_to_right; // 切换方向}
}
线索二叉树:
规律:在n个节点的链式二叉树中必定有n+1个空指针域
有序链式二叉树中有很多的空指针,可以让这些指针指向下一个、前一个节点,这样在遍历时可以不用递归而可以使用循环遍历,可以提高树的遍历速度
中序线索二叉树节点数据项:数据左子树指针右子树指针右子树指针标志位 (假表示指向真的右子树,真表示右子树指向下一个节点)实现过程:1、构建有序二叉树2、创建线索3、通过线索循环遍历二叉树
线索二叉树的实现:
#include <stdio.h>
#include<stdlib.h>
#include<stdbool.h>typedef struct TreeNode
{int data;struct TreeNode *left,*right;bool rclue;
}TreeNode;TreeNode *create_node(int data)
{TreeNode *node=malloc(sizeof(TreeNode));node->data=data;node->left=NULL;node->right=NULL;node->rclue=false;return node;
}
void _insert_tree(TreeNode **root,TreeNode *node)
{if((*root)==NULL){*root=node; return;}if(node->data<(*root)->data)_insert_tree(&(*root)->left,node);else_insert_tree(&(*root)->right,node);
}void insert_tree(TreeNode **root,int data)
{_insert_tree(root,create_node(data));
}void dlr_show(TreeNode *root)
{if(NULL==root)return ;dlr_show(root->left);printf("%d ",root->data);dlr_show(root->right);
}
//上一个节点
TreeNode *prev=NULL;//按照中序遍历,创建线索
void create_clue(TreeNode *root)
{if(NULL==root)return;create_clue(root->left);if(NULL!=prev&&NULL==prev->right){prev->rclue=true;prev->right=root;}prev=root;create_clue(root->right);
}void clue_show(TreeNode *node)
{while(node){while(node->left){node=node->left;}printf("%d ",node->data);while(node->rclue){node=node->right;printf("%d ",node->data);}node=node->right;}printf("\n");
}int main(int argc,const char* argv[])
{TreeNode *root=NULL;for(int i=0;i<10;i++){int data=rand()%100;insert_tree(&root,data);}dlr_show(root);create_clue(root);printf("\n-----------------\n");clue_show(root);
}
选择树:(胜者树、败者树)
是一种完全二叉树,待比较的数据都存储在最后一层,根节点是根据左右子树其中一个生成,因此根节点是最大或者最小的,选择树的功能是快速地找出最大值或最小值
堆:
是一种完全二叉树,不适合链式存储
大顶堆(大根堆):根节点比左右子树大
小顶堆(小根堆):根节点比左右子树小
数据项:
存储数据的内存首地址
容量
数量
操作:创建、销毁、添加、删除、空堆、满堆
堆可以实现优先队列的效果
堆实现:
#include <stdio.h>
#include<stdlib.h>
#include<stdbool.h>#define TYPE int
#define swap(a,b) {typeof(a) t=(a);(a)=(b);(b)=t;}typedef struct Heap
{TYPE *arr;int cap;int cnt;
}Heap;//创建堆
Heap *create_heap(int cap)
{Heap *heap=malloc(sizeof(Heap));heap->arr=malloc(sizeof(TYPE)*cap);heap->cap=cap;heap->cnt=0;return heap;
}//空堆
bool empty_heap(Heap *heap)
{return !heap->cnt;
}
//满堆
bool full_heap(Heap *heap)
{return heap->cnt==heap->cap;
}//添加
bool add_heap(Heap *heap ,TYPE data)
{if(full_heap(heap))return false;heap->arr[heap->cnt++]=data;//添加后调整为堆int i=heap->cnt;while(i>1){if(heap->arr[i/2-1]>heap->arr[i-1]){swap(heap->arr[i/2-1],heap->arr[i-1]); }i=i/2;}return true;
}//删除
bool del_heap(Heap *heap)
{if(empty_heap(heap))return false;//堆顶与末尾交换swap(heap->arr[0],heap->arr[heap->cnt-1]);//删除末尾heap->cnt--;//从上往下调整int i=1;while(i-1<heap->cnt){if(i*2<heap->cnt){if(heap->arr[i*2]>=heap->arr[i*2-1] && heap->arr[i-1]<heap->arr[i*2]){swap(heap->arr[i-1],heap->arr[i*2]);i=i*2+1;}else if(heap->arr[i*2]<heap->arr[i*2-1] && heap->arr[i*2-1]>heap->arr[i-1]){swap(heap->arr[i-1],heap->arr[i*2-1]);i=i*2;}elsebreak;}else if(i*2-1<heap->cnt){if(heap->arr[i*2-1]>heap->arr[i-1]){swap(heap->arr[i*2-1],heap->arr[i-1]);i=i*2;}else{break; }}elsebreak;}return true;
}//调整为大根堆
void _Heap_adjust(Heap *heap, size_t action, size_t pos)
{int i = action + 1;while (i * 2 <= pos){int j = i * 2;if (j < pos && heap->arr[j - 1] > heap->arr[j]){j++;}if (heap->arr[i - 1] <= heap->arr[j - 1]){break;}swap(heap->arr[i - 1], heap->arr[j - 1]);i = j;}
}
//查看全部
void show_heap(Heap *heap)
{for(int i=0;i<heap->cnt;i++){printf("%d ",heap->arr[i]); }printf("\n");
}//堆顶
TYPE show_top_heap(Heap *heap)
{return heap->arr[0];
}
//堆排序 循环实现
void heap_sort_for(Heap *heap)
{for(int i=heap->cnt;i>0;--i){swap(heap->arr[0],heap->arr[i-1])_Heap_adjust(heap,0,i-1);}
}//堆排序 递归实现
void _heap_justify(Heap *heap, size_t action, size_t pos)
{size_t lh = action * 2 + 1;size_t rh = action * 2 + 2;size_t largest = action;if (lh < pos && heap->arr[lh] > heap->arr[largest]) {largest = lh;}if (rh < pos && heap->arr[rh] > heap->arr[largest]) {largest = rh;}if (largest != action) {swap(heap->arr[action], heap->arr[largest]);_heap_justify(heap, largest, pos);}
}void heap_sort(Heap *heap)
{for (int i = heap->cnt - 1; i >= 0; --i){swap(heap->arr[0], heap->arr[i]);_heap_justify(heap, 0, i);}
}
int main(int argc,const char* argv[])
{Heap *heap=create_heap(10);for(int i=0;i<10;i++){int data=rand()%100;add_heap(heap,data);}show_heap(heap );printf("TOP:%d\n",show_top_heap(heap));heap_sort(heap);show_heap(heap);
}
平衡二叉搜索树(AVL树):
前提是有序的二叉树,它的左右子树的高度差不超过1,而且它的所有子树也满足这个条件
如果一个有序二叉树呈现接近单支状(类似链表),它的查找效率接近链表,因此只有达到平衡时它的查找效率最高
由于节点的值受限,因此只能通过调整达到有序,而不能进行值的修改
二叉树不平衡的基础原因:
x y
/ \ /
y t1 以y为轴向右旋转 z x
/ \ / \ /
z t2 t3 t4 t2 t1
/
t3 t4
x y/ \ / \t1 y 以y为轴向左旋转 x z/ \ / \ / \ t2 z t1 t2 t3 t4/ \t3 t4x x z/ \ / \ / \y t1 z t1 y x/ \ / \ / \ / \
t2 z y t4 t2 t3 t4 t1 / \ / \t3 t4 t2 t3
以z为轴向左旋转 以z为轴向右旋转 最终平衡x x z/ \ / \ / \t1 y t1 z x y / \ / \ / \ / \z t2 t3 y t1 t3 t4 t2/ \ / \t3 t4 t4 t2
以z为轴右旋 以z为轴左旋 最终平衡
平衡二叉树代码实现:http://t.csdnimg.cn/rrT6Z
红黑树:
也是一种自平衡的树,它不是根据子树的高度差来调整平衡的,而是给节点设置一种颜色,来达到平衡
红黑树的特性:
1、每个节点或者是黑色、或者是红色
2、根节点必须是黑色
3、每个叶子节点(NULL)是黑色
4、如果一个节点是红色,则它的子节点必须是黑色
不能有两个连续的红色节点
5、从一个节点到该节点的子孙节点的所有路径上包含了相同数量的黑色节点
保证大致上红黑树是平衡的(最长路径不超过最短路径的两倍)
红黑树插入后的调整:插入的节点一定是红色1、如果父节点是黑色,直接插入2、如果父节点是红色,需要调整a、叔叔不存在 or 叔叔为黑色进行 左旋 or 右旋祖父节点置红 父节点置黑b、叔叔存在且为红色祖父置红,父节点和叔叔置黑把祖父节点当做当前节点,继续向上讨论调整优点:插入、删除的效率比AVL树高
缺点:没有AVL树平均,查找效率没有AVL树高,但也并不差
哈夫曼树:
基本概念:
路径长度:从一个节点到另一个节点之间的路径条目数
根节点到第n层节点的路径长度为 n-1
树的路径长度:从根节点出发到每个节点的路径长度之和
节点的权:若将树中节点赋予一个有某种意义的数值,该数值称为该节点的权
节点的带权路径长度:从根节点到该节点的路径长度与该节点的权的乘积
树的带权路径长度:所有的叶子节点的带权路径长度之和,称为WPL
WPL是衡量一棵带权二叉树优劣的关键
例子:
成绩: <60 60~69 70~79 80~89 90~100
等级: E D C B A
比例: 5% 15% 40% 30% 10%普通带权二叉树的WPL:5*1+15*2+40*3+30*4+10*4=315
哈夫曼树的WPL:40+2*30+3*15+4*10+4*5 = 205哈夫曼树的目的是为了生成一棵WPL最小的的带权二叉树
构建哈夫曼树:1、把n个带权节点存入一个集合F中,把每个节点左右子树置空2、从F中选取权值最小的两个节点作为左右子树构建成一棵新的二叉树,且新的根节点的权为左右子树的权值之和3、从F中删除刚刚选出来的两个节点,把新得到的根节点放入F中4、重复2、3步骤,直到F中只剩下一棵树,既是哈夫曼树
哈夫曼编码:
目的:解决当年远距离通信(电报)的数据传输的最优解
待发送的文字:BADCA DFEED
方法1:转成二进制发送 A 000 B 001 共30个字符
方法2:
a、根据文字出现频率,构建哈夫曼树
假设频率: A27 B8 C15 D15 E30 F5
b、规定哈夫曼树的左分支为0,右分支为1,则从根节点到叶子节点经过的路径分支所组成的0、1序列为该对应字符的哈弗曼编码
哈夫曼编码:A01 B1001 C101 D00 E11 F1000
1001010010101001000111100 共25个字符
作用:数据压缩、文件压缩的其中一种方式