• 0
  • 0
分享
  • 关于数据结构,这个重要概念不了解可不行——软件测试圈
  • 曼倩诙谐 2021-11-05 10:18:26 字数 5840 阅读 775 收藏 0

  实现优先级队列最常用的数据结构是堆,堆的常见实现有二叉堆、斐波那契堆、二项堆等。

  二叉堆

  堆是一种完全二叉树,我们以小根堆为例,小根堆的性质就是,每个节点都小于其左孩子和右孩子,不难发现,这种二叉树,根的值是最小的。

  堆有以下几种操作:堆的初始化、修改某个值(规定修改之后的值小于等于原来的值)、插入某个值、取出根节点(即取出该优先队列中的优先级最高的值)。

  在进行这几种操作的时候,要维护堆的性质。

  堆的存储

  我们不难发现以下结论:在一棵完全二叉树中,假设节点下标从0开始,那么点i的左孩子的下标为 (i<<1)+1,右孩子的下标为(i<<1)+2 ,父节点的下标为(i-1)>>1,我么可以使用数组来存储这棵完全二叉树。

  向下调整

  向下调整即调整某个子树成为小根堆,由于小根堆的任意一个子树必定也是小根堆,当我们修改了位于i节点的某个值的时候,假设修改的值不小于原来的值,或者说修改的是根节点,那么如果要保持小根堆的性质,那么必定要判断这个节点是否需要移动,如果需要移动,那么必定是会移动到其子树中,不会向上移动。

  所以这时候就要将这个节点和它的两个孩子中的值较小的进行交换,由于完全二叉树的性质,右子树的深度总是小于等于左子树,所以为了这一小点优势,我们通常在两个孩子值相同时,和右孩子交换,然后这时候和他交换的这个孩子又需要调整了,显然也需要向下调整,因此我们递归调用向下调整,直到没有交换,或者到了叶子节点。这个操作的时间复杂度为O (lgn) 。

  向上调整

  向上调整和向下调整一样,适合将某个节点的值改为比以前更小或者相等的值,或者修改了叶子节点的情况。

  在这两种情况下,该节点需要向上移动,就是直接和这个节点的父节点比较,如果比父节点还小,那么就和他的父节点交换,然后递归向上调整它的父节点,直到到达根节点,或者某次没有交换。这个操作的复杂度也是O(logn)。

  初始化

  完全二叉树的一个显而易见的性质,假设其标号从0开始,到n-1结束,那么从n/2到n-1的点全是叶子节点,叶子节点可以看成是节点数为1的堆,所以说我们如果要构建一个堆,就从(n/2)-1到0点进行向下调整即可。

  初始化的时间复杂度乍一看是O(nlogn) ,调整n/2次,每次复杂度是O (logn),这不是O (nlogn) 吗?其实不然,调整n/2次不假,但是并不是每次调整都是O (logn) ,因为每次调整的复杂度取决于调整的子树的高度。

  因此,其复杂度小于O (nlogn) ,实际上初始化堆的时间复杂度为O (n) 。

  修改某值

  假设把下标为i的节点的值改为了key(修改之后的值比之前的小,也就是优先级更高),那么如果要维护堆的性质,就要在该节点处向上调整。该操作的时间复杂度为O (logn)。

  插入某值

  将新节点插入到末尾,这个新节点必定是叶子节点,那么直接在这个节点上向上调整即可。该操作的时间复杂度为O (logn) 。

  获取优先级最高的值并删除

  由于二叉堆的性质,根节点的优先级必定是最高的(即节点的值最小)所以获取优先级最高的值只需要将根节点返回即可,如果要删除掉该节点,那么就将最后一个节点放到根节点的位置,然后从根节点处向下调整即可。该操作的时间复杂度为O(logn) 。

  二叉堆实现代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Heap<T extends Comparable<T>> {
    //堆的规模
    private int n;
    //具体的堆
    private List<T> a;
    public Heap(T[] a) {
        this.a = new ArrayList<>();
        this.a.addAll(Arrays.asList(a));
        this.n = this.a.size();
        build();
    }
    /**
     * 初始化堆
     */
    public void build() {
        for (int i = (n >> 1) - 1; i >= 0; i--) {
            down(i);
        }
    }
    /**
     * 获取父节点的下标
     * @param i
     * @return
     */
    private int parent(int i) {
        return (i-1)>>1;
    }
    /**
     * 获取左孩子的下标
     * @param i
     * @return
     */
    public int left(int i) {
        return (i << 1) + 1;
    }
    /**
     * 获取右孩子的下标
     * @param i
     * @return
     */
    public int right(int i) {
        return (i << 1) + 2;
    }
    /**
     * 向下调整,以满足小根堆的性质
     * @param i
     */
    public void down(int i) {
        int left = left(i);
        int right = right(i);
        int small = i;
        if (left < n && a.get(left).compareTo(a.get(i)) < 0) {
            small = left;
        }
        if (right < n && a.get(right).compareTo(a.get(small)) < 0) {
            small = right;
        }
        if (small != i) {
            T temp = a.get(i);
            a.set(i, a.get(small));
            a.set(small, temp);
            down(small);
        }
    }
    /**
     * 向上调整
     * @param i
     */
    public void up(int i) {
        int parent = parent(i);
        if (parent >= 0 && a.get(parent).compareTo(a.get(i)) > 0) {
            T temp = a.get(i);
            a.set(i, a.get(parent));
            a.set(parent, temp);
            up(parent);
        }
    }
    /**
     * 将下标为i处的节点值更新为key(变小)
     * @param i
     * @param key
     */
    public void update(int i, T key) {
        a.set(i, key);
        up(i);
    }
    /**
     * 插入新的节点key
     * @param key
     */
    public void insert(T key) {
        a.add(key);
        n++;
        up(n - 1);
    }
    /**
     * 获取并移除根节点
     * @return
     */
    public T getTop() {
        T result = a.get(0);
        a.set(0, a.get(--n));
        down(0);
        return result;
    }
    @Override
    public String toString() {
        return a.toString();
    }
    public static void main(String[] args) {
        Integer[] a = {16, 4, 10, 14, 7, 9, 3, 2, 8, 1};
        Heap<Integer> heap = new Heap<>(a);
        System.out.println(heap);
        heap.insert(13);
        System.out.println(heap);
        heap.update(3, 1);
        System.out.println(heap);
    }
}

  斐波那契堆

  斐波那契堆(Fibonacci heap)是堆中一种,它和二项堆一样,也是一种可合并堆,可用于实现合并优先队列。斐波那契堆比二项堆具有更好的平摊分析性能,它的合并操作的时间复杂度是O (1) 。

  与二项堆一样,它也是由一组堆最小有序树组成,并且是一种可合并堆。与二项堆不同的是,斐波那契堆中的树不一定是二项树,而且二项堆中的树是有序排列的,但是斐波那契堆中的树都是有根而无序的。

1-1.png

  基本定义

typedef int Type;
typedef struct _FibonacciNode
{
    Type   key;                     // 关键字(键值)
    int degree;                     // 度数
    struct _FibonacciNode *left;    // 左兄弟
    struct _FibonacciNode *right;   // 右兄弟
    struct _FibonacciNode *child;   // 第一个孩子节点
    struct _FibonacciNode *parent;  // 父节点
    int marked;                     //是否被删除第1个孩子(1表示删除,0表示未删除)
}FibonacciNode, FibNode;

  FibNode是斐波那契堆的节点类,它包含的信息较多。key是用于比较节点大小的,degree是记录节点的度,left和right分别是指向节点的左右兄弟,child是节点的第一个孩子,parent是节点的父节点,marked是记录该节点是否被删除第1个孩子(marked在删除节点时有用)。

typedef struct _FibonacciHeap{
    int   keyNum;                   // 堆中节点的总数
    int   maxDegree;                // 最大度
    struct _FibonacciNode *min;     // 最小节点(某个最小堆的根节点)
    struct _FibonacciNode **cons;   // 最大度的内存区域
}FibonacciHeap, FibHeap;

  FibHeap是斐波那契堆对应的类。min是保存当前堆的最小节点,keyNum用于记录堆中节点的总数,maxDegree用于记录堆中最大度,而cons在删除节点时来暂时保存堆数据的临时空间。

1-2.png

  从图中可以看出,斐波那契堆是由一组小根堆组成,这些小根堆的根节点组成了双向链表(根链表),斐波那契堆中的最小节点就是"根链表中的最小节点"!

  基本操作

  插入操作

  插入操作非常简单:插入一个节点到堆中,直接将该节点插入到"根链表的min节点"之前即可;若被插入节点比"min节点"小,则更新"min节点"为被插入节点。

1-3.png

  斐波那契堆的根链表是"双向链表",这里将min节点看作双向联表的表头。在插入节点时,每次都是"将节点插入到min节点之前(即插入到双链表末尾)"。

  此外,对于根链表中小根堆都只有一个节点的情况,插入操作就很演化成双向链表的插入操作。

/*
 * 将"单个节点node"加入"链表root"之前
 *   a …… root
 *   a …… node …… root
 *
 * 注意: 此处node是单个节点,而root是双向链表
*/
static void fib_node_add(FibNode *node, FibNode *root)
{
    node->left        = root->left;
    root->left->right = node;
    node->right       = root;
    root->left        = node;
}
/*
 * 将节点node插入到斐波那契堆heap中
 */
static void fib_heap_insert_node(FibHeap *heap, FibNode *node)
{
    if (heap->keyNum == 0)
        heap->min = node;
    else
       {
        fib_node_add(node, heap->min);
        if (node->key < heap->min->key)
            heap->min = node;
    }
    heap->keyNum++;
}



作者:中国农业银行研发中心西安研发部 陈登帅   

来源:http://www.51testing.com/html/62/n-4479462.html


2021 问卷礼物图.png

  • 【留下美好印记】
    赞赏支持
登录 后发表评论
+ 关注

热门文章

    最新讲堂

      • 推荐阅读
      • 换一换
          •   国外科技媒体 Six Colors 报道,苹果 macOS 提醒和警告严重影响 Mac 设备迁移升级体验。  该媒体主编杰森?斯内尔(Jason Snell)在评测苹果 M3 iMac 和 M3 MacBook Pro 过程中,在迁移数据方面遇到了一些问题。  他表示在迁移完成之后首次重新开机 Mac 之后,出现了大量的安全警报,IT之家在此附上该媒体截图如下:  迁移助理已经迁移了我的所有应用程序,并自动启动了登录项中列出的任何应用程序或设置为在后台自动启动的应用程序。  迁移完成之后这些应用都会同时启动,并向用户发出通知提醒,告知或者提示用户这些应用的执行权限。  这些提醒和通知彼此重...
            0 0 1189
            分享
          •   本人在测试岗位工作10有余,本着对测试工作的热爱,在工作岗位上一直表现还不错,在测试技术、流程方面颇有一些心得。今天就谈谈回归测试和确认测试的区别。  一、回归测试和确认测试的误区  其实我在之前的工作当中一直都经常说“回归测试”,基本上没有提到过“确认测试”。正式接触到“确认测试”还是从学习ISTQB认证开始。ISTQB基础级大纲中就提到了确认测试和回归测试的区别。  一开始我还很疑惑,难道回归测试不就是确认测试吗?回归测试不就是在确认bug修改有没有生效吗?但是,实际上大错特错。确认测试是在修复缺陷后,在软件的最新版本上,重新执行之前因该缺陷而导致失败的测试用例,来确认缺陷被解决。而回...
            1 0 1361
            分享
          •   谷歌在今天召开的 I / O 2023 开发者大会上宣布,新版 Google Home 应用脱离仅限于邀请的公共预览阶段,现在正式面向所有人开放。  新版 Google Home 应用进行了彻底的重新设计,引入了全新的收藏夹选项卡、改进了相机界面、为现有设备提供更丰富的控件、添加了对数十种设备的支持。  更重要的是新版 Google Home 应用添加了对 Matter 设备的支持。IT之家注:谷歌一直是开发该标准的主要参与者,但自去年 Matter 推出以来,谷歌在增加支持方面比其他公司慢。  当前新版 Google Home 仅支持室内 Nest Cam 和初代 Nest Cam 室外...
            0 0 1085
            分享
          •   N+1问题:N+1问题是指在使用关系型数据库时,在获取一组对象及其关联对象时,产生额外的数据库查询的问题。其中N表示要获取的主对象的数量,而在获取每个主对象的关联对象时,会产生额外的1次查询。  N+1问题是很多项目中的通病。遗憾的是,直到数据量变得庞大时,我们才注意到它。不幸的是,当处理 N + 1 问题成为一项难以承受的任务时,代码可能会达到了一定规模。  在这篇文章中,我们将开始关注以下几点问题:  N + 1 问题的一个例子  假设我们正在开发管理动物园的应用程序。在这种情况下,有两个核心实体:Zoo和Animal。请看下面的代码片段:  @Entity   @Table(name...
            0 0 557
            分享
          •   为什么要进行自动化测试  开始正文前,我们必须先统一认知,充分认识到自动化测试的必要性,随着被测系统越来越大,逻辑越来越复杂,测试的工作量也会倍增,这必然会暴露出测试资源与测试生命周期的冲突,因此为了更快、有效、可靠的对被测系统进行测试,需要引入自动化测试。  而另一方面,当下测试开发岗位是目前软件测试的主趋势,也是升职加薪的必要手段,因此自动化测试必须要尽快实施。  分层做自动化  选择做自动化,首先要明确目标,自动化是分层的,目前主流认为,自动化测试主要分为UI、SERVICE、JUNIT 三层。  所以我们做自动化的第一步要确认,自己要做针对哪一层的自动化,每一层自动化要做的事情如下...
            15 16 1141
            分享
      • 51testing软件测试圈微信