二叉树顺序存储结构

这篇具有很好参考价值的文章主要介绍了二叉树顺序存储结构。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

目录

1.二叉树顺序存储结构

2.堆的概念及结构

3.堆的相关接口实现

3.1 堆的插入及向上调整算法

3.1.1 向上调整算法

3.1.2 堆的插入

3.2 堆的删除及向下调整算法

3.2.1 向下调整算法

3.2.2 堆的删除

3.3 其它接口和代码实现

4.建堆或数组调堆的两种方式及复杂度分析

4.1 向上调整建堆

4.1.1 建堆步骤

4.1.2 代码实现

4.1.3 时间复杂度分析 --- O(N*logN)

4.2 向下调整建堆

4.2.1 建堆步骤

4.2.2 代码实现

4.2.3 时间复杂度分析 --- O(N)

5.堆的应用

5.1 堆排序(假设升序)

5.1.1 堆排序步骤

5.1.2 代码实现

5.2 TopK问题

5.2.1 TopK解决步骤

5.2.2 代码实现(数据从文件读取)


1.二叉树顺序存储结构

顺序存储结构就是用数组来存储,一般是用数组只适合来表示完全二叉树,因为不是完全二叉树会有空间浪费的现象。

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

二叉树的顺序存储结构在物理上是一个数组,在逻辑上是一个二叉树。

现实中我们通常把堆使用顺序存储结构的数组来存储,而什么又是堆呢?

需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

2.堆的概念及结构

其实堆就是一个完全二叉树,堆中的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且满足:堆中某个结点的值总是不大于其父节点的值(大堆)或者堆中某个结点的值总是不小于其父节点的值(小堆)。

总结来说:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树;

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

注意:所有的数组都可以表示成完全二叉树,但是他并不一定是堆。

3.堆的相关接口实现


补充:

对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
1.若i>0,i位置节点的双亲序号:(i-1)/2; i=0,i为根节点编号,无双亲节点
2.若2i+1 < n,左孩子序号:2i+1;若2i+1>=n(数组越界), 无左孩子

(叶子就是没有左孩子,也就是叶子结点的左孩子下标2i+1>=n越界)
3.若2i+2 < n,右孩子序号:2i+2; 若2i+2>=n(数组越界), 无右孩子


3.1 堆的插入及向上调整算法

  1. 先将元素插入到堆的末尾,即最后一个数组元素之后
  2. 插入之后如果堆的性质遭到破坏,插入的结点就根据向上调整算法找到合适位置即可

3.1.1 向上调整算法

那么什么是向上调整算法呢?如何实现?

例如: 堆:[4, 27, 11, 28, 35, 19, 15, 89, 2] Push:2

图片展示:

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

代码实现:

void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(a + child, a + parent);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

代码注意事项:

  • 除了插入的元素,堆中的其他元素都符合堆的性质,所以调整到 待调整节点和父节点 符合堆的性质即可退出调整。
  • 循环退出条件:待调整节点调整到根节点 或者 待调整节点和父节点的大小关系符合堆的性质。
  • while语句中的条件不能写parent >= 0,因为 (0-1)/ 2 之后依旧是0,并不符合预期的循环退出条件。
  • 如果调大堆,比较符号就用>;如果调小堆,比较符号就用<;方便以后更改。

3.1.2 堆的插入

注意:堆的物理结构是一个数组,也就是用顺序表实现的,插入时容量不够要记得扩容。

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	if (php->capacity == php->size)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("HeapPush:");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

3.2 堆的删除及向下调整算法

这里堆的删除是删除堆顶数据,因为只有堆顶数据才有意义(堆顶数据都是最值,删除堆顶后能获得次大或者次小的数)

这里我们能直接删除堆顶数据吗?很明显不可以,根据顺序表删除数据的特点,后面的元素会依次覆盖前面的元素,删除堆顶数据后,堆的结构就被破坏了。

其实删除思想是这样的:

  1. 将堆顶元素与堆中最后一个元素交换
  2. 删除堆中最后一个元素
  3. 将堆顶元素向下调整直到满足堆的结构

这种思想很巧妙,在后面的堆排序中也会用到这种思想。

3.2.1 向下调整算法

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

代码实现:

void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child + 1] > a[child])
			child++;

		if (a[child] > a[parent])
		{
			Swap(a + child, a + parent);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

代码注意事项:

  • 除了堆顶元素,堆中的其他元素都符合堆的性质,所以调整到 待调整节点和左右孩子 符合堆的性质即可退出调整。
  • 循环退出条件:待调整节点调整到叶子节点 或者 待调整节点和左右孩子节点的大小关系符合堆的性质。
  • 因为要判断越界,函数参数中要有数组大小size。
  • 向下调整时,我们要和该结点的左右孩子中较小的那个交换。代码设计时我们可以先默认左孩子小,再和右孩子进行比较得出较小的那个节点。

3.2.2 堆的删除

void HeapPop(Heap* php)
{
	assert(php);
	assert(php->size > 0);

	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;
	AdjustDown(php->a, php->size, 0);
}

3.3 其它接口和代码实现

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>

typedef int HPDataType;

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

void Swap(int* a, int* b);
void AdjustUp(int* a, int child);//a:要调整的孩子结点所在的数组  child:要调整的孩子节点的下标
void AdjustDown(int* a, int size, int parent);//a:要调整的父亲结点所在的数组   size:数组的size   parent:要调整的父亲节点的下标

void HeapPrint(Heap* php);
void HeapInit(Heap* php);
void HeapDestory(Heap* php);
void HeapPush(Heap* php, HPDataType x);
void HeapPop(Heap* php);
HPDataType HeapTop(Heap* php);
int HeapSize(Heap* php);
int HeapEmpty(Heap* php);
#include "Heap.h"

void Swap(int* a, int* b)
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}

//调da堆
void AdjustUp(int* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] > a[parent])
		{
			Swap(a + child, a + parent);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

//调da堆
void AdjustDown(int* a, int size, int parent)
{
	int child = parent * 2 + 1;
	while (child < size)
	{
		if (child + 1 < size && a[child + 1] > a[child])
			child++;

		if (a[child] > a[parent])
		{
			Swap(a + child, a + parent);
			parent = child;
			child = child * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapInit(Heap* php)
{
	assert(php);

	php->a = NULL;
	php->capacity = php->size = 0;
}

void HeapDestory(Heap* php)
{
	free(php->a);
	php->capacity = php->size = 0;
}

void HeapPrint(Heap* php)
{
	for (int i = 0; i < php->size; i++)
	{
		printf("%d ", php->a[i]);
	}
	printf("\n");
}

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	if (php->capacity == php->size)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("HeapPush:");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;
	AdjustUp(php->a, php->size - 1);
}

//这里删除的是堆顶数据,只有堆顶数据才有意义
//1.swap(堆顶数据,最后一个数据)
//2.删除最后一个数据
//3.堆顶数据AdjustDown
void HeapPop(Heap* 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 HeapTop(Heap* php)
{
	assert(php);
	assert(php->size > 0);

	return php->a[0];
}

int HeapSize(Heap* php)
{
	assert(php);

	return php->size;
}

int HeapEmpty(Heap* php)
{
	assert(php);

	return php->size == 0;
}

4.建堆或数组调堆的两种方式及复杂度分析


前面我们说过,所有的数组都可以表示成完全二叉树,但是他并不一定是堆。那么我们如何将这个数组调整成堆呢?

我们首先会想到:把数组的值依次push到堆中,再把堆中数据依次赋值给数组,这样就把数组调整成了堆。但是实际应用中我们不会再写一个堆这样的数据结构,其次这种方式会有空间复杂度的消耗,所以我们不提倡这么做。

调堆方式有两种:向上调整建堆和向下调整建堆


4.1 向上调整建堆

4.1.1 建堆步骤

参考堆插入的思想,数组中的每个元素都可以看做新插入的节点。

从根结点开始调整,一直调整到最后一个结点。

想要调成成小堆:如果该结点小于父节点,就一直向上交换,直到不小于其父节点或者调整到根结点。

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

4.1.2 代码实现

int main()
{

	int a[] = { 27,15,19,28,35,11,4,89,2 };
	int size = sizeof(a) / sizeof(a[0]);
	//这里我们建小堆
	//方法一:向上调整算法
	for (int i = 0; i < size; i++)//从根结点开始调整,一直调整到最后一个结点。
	{
		AdjustUp(a, i);
	}

	for (int i = 0; i < size; i++)
	{
		printf("%d ",a[i]);
	}

	return 0;
}

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

4.1.3 时间复杂度分析 --- O(N*logN)

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK​​

4.2 向下调整建堆

4.2.1 建堆步骤

在解释向下调整算法之前,先说明一下:

向下调整算法有一个前提:左右子树必须是一个堆,才能调整。

那么我们如何调整呢?

这里我们可以利用递归思想来解决:先从倒数第一个非叶子结点的子树开始调整,一直调整到根结点的树。也就是倒着调整。

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

4.2.2 代码实现

int main()
{

	int a[] = { 27,15,19,28,35,11,4,89,2 };
	int size = sizeof(a) / sizeof(a[0]);
	//这里我们建小堆
	//方法二:向下调整算法
	for (int i = (size - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, size, i);
	}

	for (int i = 0; i < size; i++)
	{
		printf("%d ",a[i]);
	}

	return 0;
}

代码注意:

size - 1是最后一个数组元素的下标,对他减1除2后,就是他父节点的下标,也就是倒数第一个非叶子结点;

4.2.3 时间复杂度分析 --- O(N)

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

总结:向下调整算法建堆要比向上调整算法建堆要高效一些,并且向下调整算法要更常用(通常对堆顶数据进行向下调整操作),所以我们一般使用向下调整来建堆。

5.堆的应用


那么我们为什么要学堆呢?为什么要设计堆这种数据结构呢?

主要用于解决两个问题:

  • 排序
  • Topk问题:在N个数据中找最大或者最小的前k个(这里的N一般非常大)

注意:这里堆的应用问题和前面数组调堆的问题是同一个道理,我们不能使用堆数据结构的相关接口,需要在原生数组上进行操作。


5.1 堆排序(假设升序)

5.1.1 堆排序步骤

首先建堆这里就有一个坑了,正常思维来看,我们升序是建小堆,因为小堆的堆顶是最小值。

我们来看看升序建小堆的效率如何:

  1. 选出最小的数,放在第一个位置
  2. 最小的数删除后,剩下的看做一个堆。但是之前建好的关系都乱了,只能重新建堆,才能选出次小的数。

此时的时间复杂度:建堆的时间复杂度O(n),建了n次堆,时间复杂度O(n*n),这种效率还不如暴力遍历排序来的直接。

这里花里胡哨的建堆选堆顶的最值进行排序,结果效率和冒泡差不多,显然不是我们想要的结果。

那么堆排序到底是怎么排的呢,下面给出步骤

1.建堆

升序:建大堆

降序:建小堆

2.利用堆删除思想进行排序

(1)升序建的大堆,堆顶是最大元素,

(2)把堆顶(最大元素)和最后一个元素交换,

(3)最后一个元素(最大值)不看做堆中元素,堆顶元素向下调整,堆顶元素就变成了次大值,

(4)依次类推,重复(2)~(3)

可以计算一下这里的时间复杂度来和上面的建小堆方法来比较一下:

建大堆:n次向下调整,每次调整时间复杂度为O(logn),所以时间复杂度为:O(n*logn)

建小堆的时间复杂度O(n*n),很显然,数据非常多时,这两种方法的效率是天差地别

二叉树顺序存储结构,数据结构,算法,数据结构,堆,堆排序,TopK

5.1.2 代码实现

//堆排序,升序
void HeapSort(int* a, int n)
{
	//第一步:建大堆
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

	//第二步:堆删除思想进行排序(依次选数,调堆)
	for (int i = n - 1; i > 0; i--)
		//最后一步交换后就一个堆顶元素(最小值),AdjustDown没有进行调整
	{
		//将堆顶元素和最后一个元素交换,交换后最后一个元素不计入堆内
		Swap(&a[0], &a[i]);
		AdjustDown(a, i , 0);//这里第二个参数是数据个数,最后一个元素不计入堆内,正好是i
	}
}

代码注意事项:

  • 以后我们建堆都用向下调整建堆。首先高效,其次堆排序的选数调堆也用的向下调整。
  • 这里的循环变量i是最后一个元素的下标,也正好是交换后数组的元素个数(交换后最后一个元素不计入数组内)
  • 最后一步i == 1时,交换后就一个堆顶元素(最小值),AdjustDown没有进行调整,但这一步也要执行,因为a[0]和a[1]要进行交换。

5.2 TopK问题

5.2.1 TopK解决步骤

Topk问题就是在N个数中找最大或者最小的前k个。(这里的N一般非常大,大到内存装不下)

第一次碰到这个问题,我们的惯性思维会去怎么解决呢?

  • 方法一:先排降序,前k个最大
  • 方法二:N个数依次插入大堆,pop k次,每次取的都是堆顶数据

但是当N非常大时,甚至内存都放不下,很显然这两种方法不靠谱。

我们可以算一下时间复杂度:

方法一:O(N*logN)

方法二:O(N+klogN) 建堆:N,k次pop :klogN

我们直接来说说Topk问题的实际解决办法:

  1. 用数据集合的前k个元素来建堆

前k个最大的元素:建小堆

前k个最小的元素:建大堆

2.用剩余的N-k个元素依次与对顶元素比较,找最大(小)的k个:比堆顶大(小),替换,向下调整。

3.最后堆中的k个元素就是最大(最小)的k个数

计算时间复杂度:O(k+(N-k)logk)~O(Nlogk)文章来源地址https://www.toymoban.com/news/detail-709054.html

5.2.2 代码实现(数据从文件读取)

int* TopK(int k)
{
	int* retArr = (int*)malloc(sizeof(int) * k);
	//打开文件
	FILE* pf = fopen("data,txt", "r");
	if (pf == NULL) 
	{
		perror("TopK:");
		exit(-1); 
	}

	//前k个数据读入数组
	for (int i = 0; i < k; i++)
	{
		fscanf(pf, "%d", &retArr[i]);
	}

	//数组建堆(小堆)
	for (int i = (k - 2) / 2; i >= 0; i--)
	{
		AdjustDown(retArr, k, i);
	}

	//剩余N-k个数据,依次和堆顶数据比较
	for (int i = 0; i < N - k; i++) 
	{
		int x;
		fscanf(pf, "%d", &x);
		if (x > retArr[0])
		{
			retArr[0] = x;
			AdjustDown(retArr, k, 0);
		}
	}

	fclose(pf);

	return retArr; 
}

void testTopK()
{
	int* arr = TopK(10);
	for (int i = 0; i < 10; i++)
		printf("%d ", arr[i]);
	printf("\n");

	free(arr);
}

到了这里,关于二叉树顺序存储结构的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【数据结构】二叉树——顺序结构

    由于每个节点都 只有一个父节点 ,所以我们可通过双亲来表示一棵树。具体方式通过 数组的形式 实现。 根节点的下标为0 按照层序从上到下排序 每层从左向右递增 表示形式: 二维数组 数据的列标为0 ,只需确定行标,即可锁定位置 根节点的父节点下标为 -1 列标为1存父节

    2024年02月02日
    浏览(38)
  • 数据结构:二叉树的顺序结构--堆

    朋友们、伙计们,我们又见面了,本期来给大家解读一下二叉树--堆的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精通 数据结构专栏:数据结构 个  人  主  页 :stackY、 目录 前言: 1.堆的概念及

    2024年02月06日
    浏览(34)
  • 数据结构-二叉树·堆(顺序结构的实现)

    🎉个人名片: 🐼作者简介:一名乐于分享在学习道路上收获的大二在校生 🐻‍❄个人主页🎉:GOTXX 🐼个人WeChat : ILXOXVJE 🐼本文由GOTXX原创,首发CSDN🎉🎉🎉 🕊系列专栏:零基础学习C语言----- 数据结构的学习之路 🐓每日一句:如果没有特别幸运,那就请特别努力!🎉

    2024年02月05日
    浏览(36)
  • 【数据结构】二叉树的顺序结构-堆

    普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而 完全二叉树 更适合使用顺序结构存储。 现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个

    2024年02月09日
    浏览(38)
  • 数据结构——顺序二叉树——堆

            在介绍二叉树之前,我们首先要明确树是什么。         树 用我们的通常认识来判断应该是一种植物,从根向上生长,分出许多的树枝并长出叶子。对于数据结构中的树而言,其结构也正是从树的特征中剥离出来的。树结构是一种非线性数据结构,具有一个根结

    2024年01月19日
    浏览(33)
  • 【数据结构】二叉树的顺序结构及实现

    目录 1. 二叉树的顺序结构 2. 堆的概念及结构 3. 堆的实现 3.1 堆向下调整算法 3.2 堆的创建 3.3 建堆时间复杂度 3.4 堆的插入 3.5 堆的删除 3.6 堆的代码实现 4. 堆的应用 4.1 堆排序 4.2 TOP-K问题 普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉

    2024年02月08日
    浏览(32)
  • 【数据结构】树、二叉树的概念和二叉树的顺序结构及实现

    之前我们学习了顺序表、链表以及栈和队列这些数据结构,但这些数据结构都是线性的(一对一)。接下来要学习 非线性的数据结构——树(二叉树) ,相比前面的,树的结构更加复杂,话不多说,直接进入正题吧。 树是一种 非线性的数据结构 ,它是 一对多(也有可能是

    2024年02月07日
    浏览(30)
  • 数据结构初阶--二叉树的顺序结构之堆

    目录 一.堆的概念及结构 1.1.堆的概念 1.2.堆的存储结构 二.堆的功能实现 2.1.堆的定义 2.2.堆的初始化 2.3.堆的销毁 2.4.堆的打印 2.5.堆的插入 向上调整算法 堆的插入 2.6.堆的删除 向下调整算法 堆的删除 2.7.堆的取堆顶元素 2.8.堆的判空 2.9.堆的求堆的大小 三.堆的创建 3.1.向上调

    2024年02月14日
    浏览(34)
  • 初阶数据结构之---二叉树的顺序结构-堆

    今天要讲的堆,不是操作系统虚拟进程地址空间中(malloc,realloc等开空间的位置)的那个堆,而是数据结构中的堆,它们虽然名字相同,却是截然不同的两个概念。堆的底层其实是 完全二叉树 ,如果你问我,完全二叉树是什么。好吧,那我先从树开始讲起,开始我们今天的

    2024年03月14日
    浏览(47)
  • 常见的数据结构(顺序表、顺序表、链表、栈、队列、二叉树)

    线性表(Linear List)     1.什么是线性表     2.线性表的特点     3.线性表的基本运算 顺序表     1.什么是顺序表     2.时间复杂度: 链表     1.什么是链表     2.单向链表     3. 双向链表     4.ArrayList和LinkedList的使用 栈Stack     1.什么是栈     2.栈的基本方法 队列Queue

    2024年02月13日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包