数据结构 02(下)

简介: 数据结构 02

二、堆


1. 什么是堆?


堆其实也是一种基于树实现的数据结构。基于二叉树实现的堆叫做二叉堆。


2. 二叉堆:


上面说了基于二叉树实现的堆叫二叉堆,有如下特点:


  • 二叉堆是一棵完全二叉树。


  • 所有节点的父节点的值都比自己大。


我们可以使用数组来存储二叉堆,如下图。


image.png


左孩子的索引就等于父节点索引的两倍再加1,右孩子索引就是父节点索引的两倍再加2。


3. 堆的实现:


根据上面的分析,写出堆的基本结构。

public class MaxHeap<E extends Comparable<E>> {
    private Array<E> data;//存储数据的动态数组
    public MaxHeap(int capacity){
        data = new Array<>(capacity);
    }
    public MaxHeap(){
        data = new Array<>();
    }
    /** 返回堆中元素个数 */
    public int size(){
        return data.getSize();
    }
    /** 判断堆是否为空 */
    public boolean isEmpty(){
        return data.isEmpty();
    }
    /** 计算指定索引节点的父节点的索引 */
    private int parent(int index){
        if (index == 0)
            throw new IllegalArgumentException("最顶层节点无父节点");
        return (index -1) / 2;
    }
    /** 计算指定索引的左孩子的索引 */
    private int leftChild(int index){
        return index * 2 + 1;
    }
    /** 计算指定索引的右孩子的索引 */
    private int rightChild(int index){
        return index * 2 + 2;
    }
}


这里还是使用到了前一篇文章中自己实现的动态数组。接下来看看对堆的一些操作。


  • 向堆中添加元素:


添加元素直接使用动态数组的addLast方法把元素添加到最后即可,但是还要看看添加后是否还满足二叉堆的性质,看看自己是不是比自己父节点更小。如果比自己父节点更大,就需要上浮,也就是自己和父节点交换位置。那么先在动态数组添加一个交换连个元素位置的方法:

/** 交换两个元素的位置 */
public void swap(int x,int y){
     if (x<0 || x>=size || y<0 || y>=size )
         throw new IllegalArgumentException("传入的索引不合法");
     E temp = data[x];
     data[x] = data[y];
     data[y] = temp;
}


接下来就可以执行添加操作:

/** 向堆中添加元素 */
public void add(E e){
    data.addLast(e);
    siftUp(data.getSize() - 1);
}
/** 若添加的元素比父节点元素更大,就不符合完全二叉树性质,需要上浮。
 * 所谓上浮就是该元素和父节点元素交换位置。
 */
private void siftUp(int k){
    while (k>0 && data.get(parent(k)).compareTo(data.get(k)) < 0){
        data.swap(k,parent(k));
         k = parent(k);
    }



image.png

  • 删除元素:


限定只能从根节点取出元素,所以取出的元素也是最大的,因此也叫最大堆。但是删除了根节点,这棵树就变成两棵子树了,我们就把最后一个元素放到根节点的位置,让它成为根节点。但是为了满足完全二叉树的性质,就要判断这个新的根节点是否比自己的孩子更大,如果不是,就需要下沉。下沉的时候就跟它的左右孩子中较大的那个交换位置即可。

/** 查看堆中最大元素 */
    public E findMax(){
        if (data.getSize() == 0)
            throw new IllegalArgumentException("堆为空");
        return data.get(0);
    }
    /** 取出堆中最大元素 */
    public E extractMax(){
        E temp = findMax();
        data.swap(0,data.getSize() - 1);
        data.removeLast();
        siftDown(0);
        return temp;
    }
    /** 下沉操作 */
    private void siftDown(int k){
        while (leftChild(k) < data.getSize()){
            int j = leftChild(k);//左孩子索引
            //k节点的右孩子比左孩子大
            if (j+1 < data.getSize() && data.get(j+1).compareTo(data.get(j)) > 0){
                j = rightChild(k);//
            }
            if (data.get(k).compareTo(data.get(j)) >= 0)
                break;
            data.swap(k,j);
            k = j;
        }
    }


在上图添加元素的基础上删除62,就变成了这样:


image.png


  • 取出堆中最大元素后放入一个新的元素:

/** 将最大元素替换成新元素 */
    public E replace(E e){
        E temp = findMax();
        data.update(0,e);
        siftDown(0);
        return temp;
    }


这个操作比较简单,直接将索引为0处的元素更新,然后再下沉即可。


  • 将任意数组整理成堆的形状(heapify):


找到最后一个非叶子节点,从它开始做下沉操作。最后一个元素的索引的父节点索引就是最后一个非叶子节点的索引。


image.png


比如在此图中,最后一个非叶子节点是“22”,那么就是利用“62”的索引去计算“22”的索引。“22”的索引为4,那么需要做下沉操作的就是索引为4、3、2、1、0的这5个元素。上图做完下沉操作后变成:


image.png


下面看代码实现。首先在上篇文章讲的动态数组中添加一个构造函数,即传入一个数组的构造函数。

public Array(E[] arr){
        data = (E[]) new Object[arr.length];
        for (int i=0;i<arr.length;i++){
            data[i] = arr[i];
            size = arr.length;
        }
    }


然后heapify操作也写成一个构造函数。

/** heapify */
 public MaxHeap(E[] arr){
     data = new Array<>(arr);
     for (int i=parent(arr.length-1);i>=0;i--){
         siftDown(i);
     }
 }


这样就完成了heapify操作。


三、优先队列


1. 什么是优先队列?


前面讲到了队列,特点是先进先出,优先队列就是出队顺序跟入队顺序无关,与元素的优先级相关。计算机可以同时执行多任务,它在给这些应用分配内存时,其实就是使用优先队列,优先级高的任务优先使用内存,以保证带给用户最好的体验。


2. 优先队列的实现:


有了上面实现的最大堆,实现优先队列就非常简单了。因为最大堆的根节点就是优先级最高的元素,每次从最大堆中取出的也就是根节点。所以使用最大堆来实现优先队列,直接调用最大堆对应的方法就行了。

public class PriorityQueue<E extends Comparable<E>> implements Queue<E> {
    private MaxHeap<E> maxHeap;
    public PriorityQueue(){
        maxHeap = new MaxHeap<>();
    }
    @Override
    public int getSize() {
        return maxHeap.size();
    }
    @Override
    public boolean isEmpty() {
        return maxHeap.isEmpty();
    }
    @Override
    public void enqueue(E e) {
        maxHeap.add(e);
    }
    @Override
    public E dequeue() {
        return maxHeap.extractMax();
    }
    @Override
    public E getFront() {
        return maxHeap.findMax();
    }
}


其实Java也给我们提供了一个优先队列,在java.util包中有一个PriorityQueue,就是优先队列。这个PriorityQueue是一个最小堆,也就是说,每次取出是优先级最小的元素。还有就是一些方法名不同,比如入队操作是add。更多的不同请在使用过程中体会。


四、哈希表


1. 什么是哈希表?


所谓哈希表,就是将我们关心的key通过哈希函数转化成数组对应的索引,从而可以直接通过索引访问到元素。


2. 哈希函数的设计:


  • 整形:


小范围的整形key可以直接当作索引。若有负数,就进行偏移。若是大整数,比如身份证号,可以进行取模(模一个素数),所谓取模就是求余。有一个网站给出了对于指定范围内的数到底对多少取模更合适,给出该网站的网址:

https://planetmath.org/goodhashtableprimes


  • 字符串:


可以自己定义一些转换规则,将字符串转成整形来处理。


  • 引用类型:


依然可以通过自己指定的转换规则转换为整形来处理。


设计函数要遵循以下三个原则:


  • 一致性:如果 a == b,则 hash(a) == hash(b)。


  • 高效性:计算哈希函数高效简便。


  • 均匀性:哈希值均匀分布。


3. Java提供的hashCode方法:


Java的Object类中有一个hashCode方法,这个方法是根据对象在内存中地址值来计算哈希值的。基本类型需要使用其对应的包装类调用,引用类型可直接调用,也可以重写该方法,根据自己定义的规则来计算哈希值。java中HashSet和HashMap集合底层使用的就是哈希表。


4. 解决哈希冲突:


所谓哈希冲突,就是通过不同的对象通过哈希函数计算出来的哈希值可能是一样的。但是我们需要使用哈希值充当数组索引,那就不能有重复的哈希值。解决办法就是,数组每个索引位置存储的不是某一个元素,而是一个链表或者是一棵红黑树。java8中就是这样解决哈希冲突的,首先是存储链表,当哈希冲突达到一定程度时,就存储红黑树。


总结:


本文主要介绍了树、堆以及优先队列。树主要说了二分搜索树,要理解二分搜索树的操作,就要理解递归。后面说的堆其实也就是一棵树,这里说的是基于二叉树实现的最大堆。而后面说的优先队列,又是基于堆来实现的。包括《数据结构01》的源码,点即可下载。


相关文章
|
1天前
|
C++
数据结构(循环单链表
数据结构(循环单链表
7 2
|
2月前
|
存储 算法
【数据结构】什么是数据结构?
【数据结构】什么是数据结构?
13 0
|
9月前
|
存储 算法 数据库
【数据结构】初识(上)
【数据结构】初识(上)
48 0
|
4月前
|
算法
数据结构22
数据结构22
12 0
|
6月前
|
存储 算法 容器
数据结构 > 什么是数据结构?
数据结构 > 什么是数据结构?
|
8月前
|
机器学习/深度学习 算法
|
9月前
|
存储
|
9月前
|
存储 算法 C语言
|
11月前
|
存储 机器学习/深度学习 人工智能
对数据结构的初步认识
对数据结构的初步认识
114 0
|
存储 算法 安全
【数据结构】C#实现常用数据结构总结
自行整理的C#常见数据结构笔记。
323 0
【数据结构】C#实现常用数据结构总结