二叉树与堆的实现

1.1二叉树的顺序结构

顺序结构, 也就是以顺序表的形式储存,但是普通的二叉树不会选择用顺序表实现,会有空间的浪费,而完全二叉树非常适合用顺序表实现。

1.2二叉树的链式结构

typedef int BinaryTreedatatype;
typedef struct BinaryTreenode
{
	BinaryTreedatatype val;
	struct Tree* leftchild;
	struct Tree* rightchild;
}BN;

链式结构也就是像链表的形式,二叉树决定了它的度不会高于2,所以在用链式结构实现的时候并没有一般的树那种问题,也就是不同的节点有不同的孩子个数。

我们统一的用上面的方式表示每一个节点。

下面我们介绍二叉树顺序结构的应用——堆

1.3堆与堆的实现

堆是一种完全二叉树,他用数组的方式存储。堆又分为小堆和大堆,小堆是指父亲结点的值总小于等于左右孩子的值,反之是大堆。

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

堆的删除是从堆顶删除数据,也就是删除a[0],插入时只需要尾插就好。

但是我们每次插入和删除后并都能保证操作完还是一个堆。比如删除的时候,我们需要从头部删除一个数据,但是物理存储的时候只是一个数组,如果我们只是单纯删除掉一个头部的元素,那么顺序表中的结构会产生一些变化。

例如:

int a[] = {1,5,3,8,7,6};//删除前的数组

如果我们删除后只是把后面的元素前移,逻辑上的顺序就变成了。

首先这个堆不再是一个小堆,其次结点之间的关系被打乱了,5和3原本是兄弟节点,现在变成了父子节点。为了使堆仍然保持之前的小堆(大堆),我们改变删除的思路,选择交换头尾两个元素,然后直接删除掉尾,再把头部的元素向下调整。

假设我们的堆是个小堆,我们要找到左右孩子中最小的,把父亲和孩子交换,然后把孩子的下标给父亲,继续计算新的孩子的下标。父亲比孩子小就结束循环。

void Swap(Heapdatatype* x, Heapdatatype* y)
{
	Heapdatatype tmp = *x;
	*x = *y;
	*y = tmp;
}
void Adjustdown(Heapdatatype* a, int size, int parent)
{
	int child = 2 * parent + 1;
	while (child < size)
	{
		if (a[child] > a[child + 1])//选出左右孩子中最小的一个
		{
			child++;
		}
		if (a[child] < a[parent])//父亲比孩子大就交换
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;//计算新的孩子的下标
		}
		else//不满足条件就跳出循环
		{
			break;
		}
	}
}

思路还是比较简单的,但是上面的代码还有一个小缺陷——就是我们并不能保证左右孩子都存在。所以第一个if条件需要修改。

void Adjustdown(Heapdatatype* a, int size, int parent)
{
	int child = 2 * parent + 1;
	while (child < size)
	{
		if (child+1 < size && a[child] > a[child + 1])
		{
			child++;
		}
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

我们来测试一下,

结果是正确的。与此同时,我们的pop函数也就完成了。

void Heappop(Hp* p)
{
	assert(p);
	assert(!HeapEmpty(p));
	Swap(&p->a[0], &p->a[p->size - 1]);
	Adjustdown(p->a, p->size, 0);
}

堆还有一个重要的接口函数就是push,每次我们push进去的时候都要保证堆仍然是一个小堆(大堆),所以如果push进去的元素比较小,我们就要把它向上调整,思路和上面基本一样,我直接给出代码。

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

堆的push

void Heappush(Hp* p, Heapdatatype x)
{
	assert(p);
	//先考虑堆是不是为空
	if (p->size == p->capacity)
	{
		int newcapacity = p->capacity == 0 ? 4 : 2 * p->capacity;
		p->capacity = newcapacity;
		p->a[p->size++] = x;
	}
	else
	{
		p->a[p->size++] = x;
	}
	Adjustup(p->a, p->size);
}

堆还有几个接口函数,包括初始化,销毁,判空,堆的大小,取堆顶的数据,这些都比较简单,就不多赘述了。

1.4向上调整和向下调整算法的时间复杂度

下面我们考虑满二叉树的情况,计算最坏情况的时间复杂度。

向上调整时,每一层要最多有2^(i-1)个元素要调整,最多要向上调整i-1层,每一层要调整(i-1)*(2^(i-1))次,假设总共有k层,结点个数为n。

将每一层的情况求和,得到时间复杂度是O(n*logn)。

同理对向下调整进行计算,可以得到时间复杂度是O(n)。

接下来我们来讲如何对已有的数组进行建堆,目的为后面讲topK问题和堆排序打下基础。

1.5堆的创建

上面已经计算了调整算法的时间复杂度,自然我们建堆的时候应该选择向下调整算法,假设我们要建一个小堆,现在的问题是向下调整的前提是左右子树都应该是小堆。所以我们处理的方法是——从最后一个子树开始,逐个创建小堆。

void HeapCreate(Heapdatatype* a, int n)
{
	//从最后一个非叶子节点开始每个树做调整。
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		Adjustdown(a, n, i);
	}
}

找到最后一个非叶子节点,计算它的父亲结点,对这个父亲结点的树进行向下调整,遍历所有树,就可以调整完。

讲完堆的创建我们就可以开始讲堆的应用之一——堆排序了!
1.6堆排序

如果我们要建一个升序,那么我们要建大堆,每次都把堆顶的元素放到最后一个元素,然后不考虑最后一个元素,其余的元素视为一个新的堆,然后进行向下调整。

void Heapsort(Heapdatatype* a, int n)
{
	for (int i = (n - 1 - 1) / 2;i >= 0;i--)
	{
		Adjustdown(a, n, i);
	}
	int End = n - 1;
	while (End >= 0)
	{
		Swap(&a[0], &a[End]);//交换首尾的元素
		Adjustdown(a, End, 0);//对前n-1个元素构成的堆进行向下调整的算法——目的是选出第二大的元素堆顶
		End--;
	}
}

1.7TopK问题

生活中时常见到这些字眼——必打卡的TOP10网红景点,福建省TOP3美食..等等 这些都是TOPK算法的结果,但是实际中的算法比我们现在讲到的更复杂,需要考虑更多方面的问题。

TOPK算法的三种思路——1、堆排序 2、建立一个N个数的大堆, POP出K个元素。3、建立一个K个元素的堆(如果是升序就建大堆,降序建小堆),分别将后N-K个元素依次与堆顶的元素比较,如果比堆顶的数大就与堆顶元素交换,最后留在堆中的K个数就是前K个。

我们大概来看一下三个算法的时间复杂度和空间复杂度。

堆排序的建堆过程大概是O(N),第二个循环的复杂度是O(N*logN),所以总体的复杂度是O(N*logN)。空间复杂度是O(N)。

第二个思路的时间复杂度是O(N+K*logN)~O(N)。空间复杂度是O(N)。

第三个思路的时间复杂度O(K+(N-K)*logK) ~ O(N)。空间复杂度是O(1)。

可以看出来第三个思路虽然时间复杂度并不会比第二个优化很多,但是空间复杂度降低很多。所以我们选择第三种思路实现。

void TopK(int* a,int n,int k)
{
	//先建一个K个数的小堆,再把剩下的N-K个数依次和堆顶的数比较,如果大于堆顶的数就进堆。
	int* p = (int*)malloc(sizeof(int) * k);
	for (int i = 0;i < k;i++)
	{
		p[i] = a[i];
	}
	for (int i = (k - 1 - 1) / 2;i >= 0;--i)
	{
		Adjustdown(p, k, i);
	}
	for (int i = k;i < n;i++)
	{
		if (a[i] > p[0])
		{
			p[0] = a[i];
			Adjustdown(p, k, 0);
		}
	}
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值