[数据结构与算法]常用数据结构——二叉树,二叉大小堆和堆排序

常用数据结构——二叉树,堆栈,大小堆和堆排序

最近刷leetcode有一道题需要用到最大堆,最小堆。其中用到了一个黑科技,一直想不出来,关于最大堆任意元素的删除(并非堆顶的删除),直到在网上看到了一个算法,顺便回顾一下大小堆。

参考:
https://www.jianshu.com/p/6d3a12fe2d04

二叉树

在数据结构中,二叉树是每个结点最多有两个子树的树结构,其中每个结点保存的有结点存储的数据,以及指向两个子树的指针(也可以保存父结点的指针)。通常子树被称作“左子树”和“右子树”。

二叉树的每个结点至多只有两棵子树,且左右子树有顺序之分不能颠倒。

性质:
  • 二叉树的第i层至多有 2 i − 1 2^{i-1} 2i1个结点。
  • 深度为k的二叉树至多有 2 k − 1 2^k-1 2k1个结点。
  • 对任何一棵二叉树,如果其末端结点数为 n 0 n_0 n0(末端结点:无子结点),度为2的结点数为 n 2 n_2 n2(度为2的结点:具有左右两个子结点),则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

注1:关于第三条的证明:任何一个度为1的结点都可以被看作只是连接的线,进而全部可以忽略不计,那么原始的树可以被退化为只有末端结点和度为2的结点。之后,由于每一个度为2的结点都有两个子结点,从末端结点的父结点开始,将其父结点和左子结点抵消掉,将右结点放到父结点上面,递归退化,最终退化完发现只剩一个末端结点,故末端结点个数比度为2的结点个数大1。

树和二叉树的主要差别:

  • 树的结点个数至少有1个,二叉树的结点个数可以为0。
  • 树中结点的最大度数没有限制,二叉树结点的最大度数为2。
  • 树的结点无左右等顺序之分,二叉树有左右之分。

满二叉树

在这里插入图片描述

满二叉树与完全二叉树示意图

一棵深度为 k k k,且有 2 k − 1 2^k-1 2k1个节点的树为满二叉树。(叶子结点都在最后一层,且除了叶子结点以外,每一个结点都有左右子结点)

所以满二叉树的外形是一个标准的三角形。

性质:
  • 满二叉树的深度 h h h和总层数 k k k相同。
  • 叶子数为 2 h − 1 2^{h-1} 2h1
  • k k k层的结点数为 2 ( k − 1 ) 2^(k-1) 2(k1)
  • 总结点数为 2 h − 1 2^h-1 2h1
  • 总结点数为奇数
  • 树高 h = l o g 2 ( n + 1 ) h=log_2(n+1) h=log2(n+1)

注1:关于树高的计算十分重要,因为很多算法的时间复杂度都与树高有关。

完全二叉树

完全二叉树是由满二叉树引申出来的(弱化版本)。对于深度为k的,有n个结点的二叉树。当且仅当每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应,则称其为完全二叉树。
(若设二叉树的深度为h,除第h层外,其他各层的结点数都达到了最大个数,且第h层所有的结点都连续集中在最左边,这就是完全二叉树。)
在这里插入图片描述

满二叉树与完全二叉树示意图

所以满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

性质:
  • 深度为k的完全二叉树至少有 2 k − 1 2^{k-1} 2k1个结点,至多有 2 k − 1 2^{k}-1 2k1个结点
  • 树高 h = ( i n t ) l o g 2 ( n ) + 1 h=(int)log_2(n)+1 h=(int)log2(n)+1
  • 由于完全二叉树没有空间的稀疏,即每一个结点的位置都可以被下标 n n n唯一确定,所以储存时除了可以使用Node结点的结构来使用,也可以使用线性表来存储。

此处的堆是数据结构中的堆,而不是内存模型中的堆。

内存模型中的堆:

一、堆栈空间分配区别:
  1、栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
  2、堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
  二、堆栈缓存方式区别:
  1、栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
  2、堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

数据机构中的堆:

堆通常可以被看作一棵树,其满足性质如下:

  • 堆中任意结点的值总是不大于(不小于)其子节点的值
  • 堆总是一颗完全树

将任意节点不大于其子节点的堆叫做最小堆或小根堆,而将任意节点不小于其子节点的堆叫做最大堆或大根堆。常见的堆有二叉堆、左倾堆、斜堆、二项堆、斐波那契堆等等。

二叉堆

二叉堆是完全二叉树,其分为最大堆和最小堆。最大堆:父结点的值总是大于或等于任何一个子结点的值。(非常适合做优先队列,或者查找某一数组第k大的数)

在这里插入图片描述

最大堆

通常二叉堆都是由线性表(数组)来存储的,如上图的存储为:

int[] val = new int[]{9,8,7,6,5,4,3,2,1};

最大堆一般是有三种操作,插入、删除和得到堆顶元素。

插入(上浮):

为了保持最大堆完全二叉树的性质,插入时一般放在最后一位,之后对其执行上浮操作。即比较其于父节点的大小,如果大于父节点,则交换其与父节点的位置。

删除(下沉):

为了保持最大堆的性质,最大堆删除的时候只能删除堆顶数据。之后将最后一位数据放在堆顶,执行下沉操作,即其小于左右子节点中某一个时,将子节点中较大的一个与其交换位置。

得到堆顶元素:

只用返回最大堆的第一个元素即可。

class maxHeap {
        int[] val;
        int size = 0;
        maxHeap (int k) {
            val = new int[k];
        }

        public void insert(int x) {
        	// 将新元素放在最后一位上
            int i = size;
            // 将元素于父结点进行对比执行上浮操作
            while (i > 0 && x > val[(i-1)/2]) {
                val[i] = val[(i-1)/2];
                i = (i-1)/2;
            }
            val[i] = x;
            size++;
        }


        public void delete() {
        	// 删除堆顶元素
            int i = 0;
            int tmp = val[size-1];
            val[size-1] = 0;
            // 堆的大小减1
            size--;
            // 将元素于左右子结点进行对比执行下沉操作
            while ((i+1)*2 < size && (tmp < val[(i+1)*2-1] || tmp < val[(i+1)*2])) {
                // 对左右子结点进行对比
                if (val[(i+1)*2-1] > val[(i+1)*2]) {
                    val[i] = val[(i+1)*2-1];
                    i = (i+1)*2-1;
                } else {
                    val[i] = val[(i+1)*2];
                    i = (i+1)*2;
                }
            }
			// 考虑到子节点可能只有左结点存在
            if ((i+1)*2-1 < size && tmp < val[(i+1)*2-1]) {
                val[i] = val[(i+1)*2-1];
                i = (i+1)*2-1;
            }
            val[i] = tmp;
        }


        public int get() {
            return val[0];
        }
    }
删除特定的元素(黑科技):

假如说,我们要删除某一个特定元素,这个元素可能不在堆顶,但具体在哪里我们也不知道。考虑到时间复杂度,不能每次都重新删除元素后再构造新的最大堆,所以就有了如下黑科技。

在每次删除元素的时候,并不是在堆的数组中删除,而是重新构建一个Map用来存储已经被删除的元素,进而当得到堆顶元素时,查询Map,如果其已经被删除了,那么就执行堆的删除操作,再重新查询。直到堆顶元素不存在于Map中或者Map中不需要删除这个元素。

import java.util.*;

public class maxHeap {
	private int[] val;
	private int size = 0;
	private Map<Integer, Integer> map = new HashMap<>();

	public maxHeap (int k) {
		val = new int[k];
	}

	public void insert(int x) {
		// 将新元素放在最后一位上
		int i = size;
		// 将元素于父结点进行对比执行上浮操作
		while (i > 0 && x > val[(i-1)/2]) {
			val[i] = val[(i-1)/2];
			i = (i-1)/2;
		}
		val[i] = x;
		size++;
	}

	public int pop() {
		int tmp = this.get();
		this.erase(tmp);
		return tmp;

	}

	private void delete() {
		// 删除堆顶元素
		int i = 0;
		int tmp = val[size-1];
		val[size-1] = 0;
		// 堆的大小减1
		size--;
		// 将元素于左右子结点进行对比执行下沉操作
		while ((i+1)*2 < size && (tmp < val[(i+1)*2-1] || tmp < val[(i+1)*2])) {
			// 对左右子结点进行对比
			if (val[(i+1)*2-1] > val[(i+1)*2]) {
				val[i] = val[(i+1)*2-1];
				i = (i+1)*2-1;
			} else {
				val[i] = val[(i+1)*2];
				i = (i+1)*2;
			}
		}
		// 考虑到子节点可能只有左结点存在
		if ((i+1)*2-1 < size && tmp < val[(i+1)*2-1]) {
			val[i] = val[(i+1)*2-1];
			i = (i+1)*2-1;
		}
		val[i] = tmp;
	}

	public void erase(int x) {
		// 将删除元素存入Map中
		if (map.containsKey(x)) {
			map.put(x, map.get(x)+1);
		} else {
			map.put(x, 1);
		}
	}

	public int get() {
		// 获得堆顶元素,并将其于已删除元素进行对比。
		if (size == 0) {
			throw new NegativeArraySizeException("堆已为空");
		}
		int tmp = val[0];
		while (map.containsKey(tmp) && map.get(tmp) > 0) {
			map.put(tmp, map.get(tmp)-1);
			this.delete();
			tmp = val[0];
		}
		return tmp;
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值