数据结构八大类型的总结及应用
数据结构八大类型的总结及应用(下)
本篇博客只总结了后一半的内容,前一半的链接给各位送上:
https://blog.csdn.net/weixin_44593531/article/details/107444747
树
树是一种数据结构,由n个结点组成的具有层次关系的模型。具有以下特点:
- 每个结点都有0个或者多个子结点,该结点所拥有的子节点的个数被称为结点的度。
- 没有父节点的结点叫做根节点,除了根节点之外,每个节点只有一个父节点。
二叉树
二叉树是一种特殊形式,其特殊性表现在如下几个方面:
- 每个结点的度最大为2
- 左子树和右子树不能颠倒,即使只有一个子树,也要分左子树和右子树。
二叉树在添加和删除元素上都很快,而且可以通过前序、中序、后序的方法遍历和查找,因此,二叉树不仅具有链表的好处,还包含了数组的好处,在处理动态数据方面非常有用。
二叉树的遍历
主要分为三种方式:前序遍历,中序遍历,后序遍历。
在这里我给出一个实例,来加深对三种遍历方法的认识:
前序遍历
前序遍历主要是以:根节点—左子树—右子树的顺序遍历,在遍历过程中,每一个子树也需要以这样的方式进行遍历。
具体过程如下:
首先将根节点按照根—左---右的方式查找,应该是ABC,但是当遍历到左子树的时候还存在其他的以左结点为根的子树也应该按照根—左---右,因此应该先对左子树做完遍历,在遍历C,以这样的思想,就有了ABDGEHICFJ的遍历。
中序遍历
中序遍历主要是以:左子树—根---右子树的顺序,每一个子树也需要以这样的方式遍历。
具体思路:
首先从根节点开始以左—根---右的顺序,也就是说,在每一对父子关系上面都应该左子结点先于父节点,右子节点后于父节点,因此B先于A出现,D先于B出现,G先于D出现,其他部分也同理,因此就有了GDBHEIACJF
后序遍历
后序遍历主要是以:左子树—右子树—根,每个子树也要遵循该原则。
思路如下:
从根结点开始,根—左---右。也就是说左右子树都要先于A出现,D和E先于B出现,HI先于E 出现,等等。因此得到结果:GDHIEBJFCA
三种遍历方式的代码实现(递归)
这里我主推递归思想,因为我们在遍历的思考中就是要从根节点出发,每遇到一个结点然后去考虑以它为根的树的遍历顺序,因此递归实现会比较顺利。
三种遍历二叉树的方式,使用C语言代码实现如下:
void PreOrderTree(BiTree root)//先序遍历
{
if (root != NULL)
{
printf("%c", root->data);//先遍历根节点
PreOrderTree(root->LChild);//然后是左子树
PreOrderTree(root->RChild);最后是右子树
}
}
void InOrderTree(BiTree root)//中序遍历
{
if (root != NULL)
{
InOrderTree(root->LChild);//先遍历左子树
printf("%c", root->data);//遍历根
InOrderTree(root->RChild);//遍历右子树
}
}
void PostOrderTree(BiTree root)//后序遍历
{
if (root != NULL)
{
PostOrderTree(root->LChild);//先遍历左子树
PostOrderTree(root->RChild);//遍历右子树
printf("%c", root->data);//遍历根节点
}
}
几种特殊的二叉树
二叉树中还包括几种特殊的二叉树,我下面给各位读者介绍几种简单常见的二叉树
完全二叉树
完全二叉树指的是每一个结点的度都是0或者是2,也就是说不存在某个结点只有左子树或者只有右子树。完全二叉树经常出现在堆结构中,常用于堆排序。
二叉排序树
又叫做二叉查找树或二叉搜索树。包含以下特征:
- 若他的左子树非空,则左子树的所有节点的关键字均小于跟结点的关键字。
- 若他的右子树非空,则右子树的所有节点的关键字均大于跟结点的关键字。
- 左右子树分别也是二叉排序树。
对二叉排序树进行中序遍历时,就可以得到顺序数列。
二叉排序树的生成递归算法
void insert(BiTree *T,int x){
if(T==NULL){
T=(*BiTree)malloc(sizeof(BiTNode));
T->elem = x;
T->lchild = T->rchild = NULL;
}else if(k<T->data){
insert(T->lchild,x);
}else if(k>T->data){
insert(T->rchild,x);
}
}
最优二叉树
也称作哈夫曼树,一群带有权值的叶节点,所构成的最小带权路径长度(WPL)的二叉树。
如何使用一群带有权值的叶节点生成最优二叉树:
首先在集合中选择最小的两个,作为左右子树,他们的和就是该根节点的权值,然后删除刚刚使用过的结点并把新生成的权值加入到该集合中,然后重复上述步骤,一直到获得根节点的权值。
散列表
散列表,也叫哈希表,是根据关键码和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。
散列技术又被称为哈希或杂凑,他既是一种存储方式也是一种查找方式。我们可以通过取模运算,将许多数据存放在一串连续的地址上,模是几就放在索引是几的位置上,理想状态下,我们可以一次找到想要的数据。
但是散列表会产生冲突,比如在求模运算之后出现模相同的情况,两个数据应该放到同一个位置上,这叫做冲突,因此就必须有解决冲突的方法:
开散列法
就是将发生冲突的数据用一个单链表连接起来,形成一个同义词链,在需要查找的时候查找该key下所对应的单链表以查找元素。
闭散列法
就是把发生冲突的元素放在其他位置,放在什么位置上呢?分为以下两种:
线性探查法
就是查找该位置下的下一个有没有,没有的话放入,有的话继续向下一个查询,直到找到一个没有数据的位置放入。
二元探查法
就是当发生冲突时,在该位置的左右来回探查,以正负1的平方,正负2的平方依次向下寻找。
双散列法
聚集的问题可以使用双散列法解决,这回方法使用两个散列函数,一个计算散列地址,一个解决冲突
随机探查法
采用随机探查法解决冲突,求下一个地址的公式可以写成H(i)=(H+i*RH(k))%m
堆
堆是一种比较特殊的数据结构,可以看作一棵树的数组对象,一般将根节点最大的堆称为大顶堆,根节点最小的称为小顶堆。而且堆是一个完全二叉树。
堆的定义如下:n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4…n/2),满足前者的表达式的成为小顶堆,满足后者表达式的为大顶堆。
因为堆有序的特点,一般用来做数组中的排序,称为堆排序。
堆排序
利用堆的有序性,例如一个大顶堆,我们把堆顶元素放入一个有序的区域,并把剩下的元素重新生成一个堆,然后把堆顶元素再取出放入有序区域,以此类推,每次排序一个元素,最后所有的元素都可以排好。
该实例存入的数据下表是从1开始的,parent访问二叉树左右子节点分别是2parent和2parent+1
void HeapAdjust(int H[],int start,int end)//堆调整,将start和end之间的元素调整为最大堆
{
int temp=H[start];
int parent=start,child;
while(2*parent<=end)
{
child=2*parent;
if(child!=end&&H[child]<H[child+1])++child;
if(temp>H[child])break;
else H[parent]=H[child];
parent=child;
}
H[parent]=temp;
}
void HeapSort(int H[],int L,int R)
{
for(int i=(R-L+1)/2;i>=L;--i)//调整整个二叉树为最大堆
HeapAdjust(H,i,R);
for(int i=R;i>=L;--i)
{
swap(&H[L],&H[i]);
HeapAdjust(H,L,i-1);
}
}
图
图是由顶点的有穷非空集合和顶点之间边的集合组成, 通常表示为: G(V,E), 其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
- 在线性结构中,每个元素只有直接先驱和直接后继;
- 在树形结构中,数据元素之间有着明显的层次关系,每个元素作为根可以和多个元素产生关联,但是子节点和根元素只能是多对一的关系,不可以一对多。
- 但是在图形结构中,数据元素之间可以有任意关系。因此,为了描述关系复杂的结构,我们使用图来实现。
图在边的方向上主要分为有向图和无向图,图在边的权上分为有权图和无权图:
有向图
顶点之间的边有方向,称之为有向边。所有边都是有向边的图叫做有向图。
无向图
顶点之间的边没有方向,这条边就叫做无向边。
有权图
每一条边上都有所对应的数字(例如:路费、距离等)
无权图
主要是描述相对关系,边上无数字。
相关概念:
如果两个顶点在同一条边上,则称它们互为邻接点。对于无向图,顶点v的度就是和v相关联的边的条数,对于有向图,顶点v的度分为入度和出度。
图的遍历方式
图的搜索(遍历)方式主要有两种,一种叫做深度优先搜索(DFS),另一种是广度优先搜索(BFS)
图的深度优先搜索
深度优先遍历就是首先从某一个节点作为起点,然后选择一条路径走到最后,然后返回上一个分岔路口,选择你走过的另一条路,然后再走到最后,以此类推,直到遍历结束。本图的深度优先遍历结果就是A、B、C、D、F、E(也可以是A、B、D、F、E、C,种类不唯一)
图的广度优先搜索
广度优先遍历就是首先从某一个节点作为起点,然后先走遍与他直接相连的每个节点,在往下走,再走与刚才走过的节点直接相连的节点,以此类推,直到遍历结束。本图的广度优先遍历结果就是A、C、B、D、E、F(也可以是A、B、C、D、E、F,种类不唯一)
图的应用
图是一种比较复杂的数据结构,在存储数据上有着比较复杂和高效的算法,分别有邻接矩阵 、邻接表等存储结构。
实例:
以邻接表的形式给定一个无向图graph,当这个图为二分图时返回true。
使用Go语言执行深度优先搜索:
var (//定义全局变量
UNCOLORED, RED, GREEN = 0, 1, 2
color []int
flag bool
)
func isBipartite(graph [][]int) bool {
n := len(graph)
flag = true
color = make([]int, n)
for i := 0; i < n && flag; i++ {
if color[i] == UNCOLORED {
dfs(i, RED, graph)
}
}
return flag
}
func dfs(index, c int, graph [][]int) {
color[node] = c
cNei := RED
if c == RED {
cNei = GREEN
}
for _, neighbor := range graph[node] {
if color[neighbor] == UNCOLORED {
dfs(neighbor, cNei, graph)
if !flag {
return
}
} else if color[neighbor] != cNei {
flag = false
return
}
}
}
使用java执行广度优先搜索:
class Solution {
private static final int UNCOLORED = 0;
private static final int RED = 1;
private static final int GREEN = 2;
private int[] color;
public boolean isBipartite(int[][] graph) {
int n = graph.length;
color = new int[n];
Arrays.fill(color, UNCOLORED);
for (int i = 0; i < n; ++i) {
if (color[i] == UNCOLORED) {
Queue<Integer> queue = new LinkedList<Integer>();
queue.offer(i);
color[i] = RED;
while (!queue.isEmpty()) {
int node = queue.poll();
int cNei = color[node] == RED ? GREEN : RED;
for (int neighbor : graph[node]) {
if (color[neighbor] == UNCOLORED) {
queue.offer(neighbor);
color[neighbor] = cNei;
} else if (color[neighbor] != cNei) {
return false;
}
}
}
}
}
return true;
}
}