外部排序,败者树多路归并

最近面试官问到的一个问题,5G数据,2G内存,排序。当时想到了多路归并,却不知道败者树的存在,但实际上想到了一个类似堆的数据结构能优化k路数据的比较找出最小值,可惜比较紧张没深入想下去。

败者树的思路和堆的相近,特点是树的结点保存败者(如果找最小,较大值就是败者),胜者向上传递,最后根节点的父节点保存最后胜者(tree[0]的值)。然后通过下标找到相应归并段,把队首元素取出(队列中下一个元素自然成为即将加入败者树的元素,进行又一轮调整找最小值)。

归并段的数据写入到输入缓冲区,用队列存储,输出缓冲区保存每次调整树后找到的胜者,完成外部排序。时间复杂度O((n-1)log(k))

注意:和堆的结构以及调整方式是不一样的,所以只是思路相近。

 

这里写图片描述

参考了别人的代码实现,但是修改了注释更容易理解,图中ls[] 对应代码中的tree[] 数组,图中b[]对应代码中leaves列表。

参考链接: https://blog.csdn.net/liqing0013/article/details/93473266

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
/*
 * 根节点的父节点为最小值的败者树,非叶子节点保存左右子树上传的值中的较大值(败者),较小值继续向上传递
 * 完全二叉树 k路归并k个叶节点,k-1个非叶子节点+1个根节点的父节点
 */
public class LoserTree<T extends Comparable> {
    /**
     * 败者树(不包括叶子节点),保存下标    	tree[0]保存最小值下标
     */
    private Integer[] tree = null;
    /**
     * 叶子节点数组的长度(即 k 路归并中的 k)
     */
    private int size = 0;
    /**
     * 叶子节点(必须是可以比较的对象) 
     */
    private ArrayList<T> leaves = null;
    /**
     * 初始化的最小值
     */
    private static final Integer MIN_KEY = -1;

    /**
     * 败者树构造函数
     *
     * @param initValues 初始化叶子节点的数组,即各个归并段的首元素组成的数组
     */
    public LoserTree(ArrayList<T> initValues) {
        this.leaves = initValues;
        this.size = initValues.size();
        this.tree = new Integer[size];

        //初始化败者树(严格的说,此时它只是一个普通的二叉树)
        for (int i = 0; i < size; i++) {
            //初始化时,树中各个节点值设为可能的最小值
            tree[i] = MIN_KEY;
        }
        //初始化 要从最后一个节点开始调整  k次调整   
        for (int i = size - 1; i >= 0; i--) {
            adjust(i);
        }

    }

    /**
     * 从底向上调整树结构
     *
     * @param s 叶子节点数组的下标
     */
    private void adjust(int s) {
        // tree[t] 是 leaves[s] 的父节点       
        int t = (s + size) / 2;
        while (t > 0) {
        									//如果叶子节点值大于父节点(保存的下标)指向的值
            if (s >= 0 && (tree[t] == -1 || leaves.get(s).compareTo(leaves.get(tree[t])) > 0)) {
                //父节点保存其下标:总是保存较大的(败者)。 	较小值的下标(用s记录)->向上传递
                int temp = s;
                s = tree[t];
                tree[t] = temp;
            }
            // tree[Integer/2] 是 tree[Integer] 的父节点
            t /= 2;
        }
        //最后的胜者(最小值)
        tree[0] = s;
    }

    /**
     * 给叶子节点赋值
     *
     * @param leaf 叶子节点值
     * @param s    叶子节点的下标
     */
    public void add(T leaf, int s) {
        leaves.set(s, leaf);
        //每次赋值之后,都要向上调整,使根节点保存最小值的下标(找到当前最小值)
        adjust(s);
    }

    /**
     * 删除叶子节点,即一个归并段元素取空
     *
     * @param s 叶子节点的下标
     */
    public void del(int s) {
        //删除叶子节点
        leaves.remove(s);
        this.size--;
        this.tree = new Integer[size];

        //初始化败者树(严格的说,此时它只是一个普通的二叉树)
        for (int i = 0; i < size; i++) {
            //初始化时,树中各个节点值设为可能的最小值
            tree[i] = MIN_KEY;
        }
        //从最后一个节点开始调整
        for (int i = size - 1; i >= 0; i--) {
            adjust(i);
        }

    }

    /**
     * 根据下标找到叶子节点(取值)
     *
     * @param s 叶子节点下标
     * @return
     */
    public T getLeaf(int s) {
        return leaves.get(s);
    }

    /**
     * 获得胜者(值为最终胜出的叶子节点的下标)
     *
     * @return
     */
    public Integer getWinner() {
        return tree.length > 0 ? tree[0] : null;
    }


    public static void main(String[] args) {
        //假设当前有 4 个归并段
        Queue<Integer> queue0 = new LinkedList();
        Queue<Integer> queue1 = new LinkedList();
        Queue<Integer> queue2 = new LinkedList();
        Queue<Integer> queue3 = new LinkedList();
        Integer[] source0 = {2, 8, 16, 23, 26};
        Integer[] source1 = {4, 13, 22, 23, 29};
        Integer[] source2 = {5, 12, 15, 23, 32};
        Integer[] source3 = {3, 7, 17, 23, 28};
        queue0.addAll(Arrays.asList(source0));
        queue1.addAll(Arrays.asList(source1));
        queue2.addAll(Arrays.asList(source2));
        queue3.addAll(Arrays.asList(source3));

        Queue<Integer>[] sources = new Queue[4];
        sources[0] = queue0;
        sources[1] = queue1;
        sources[2] = queue2;
        sources[3] = queue3;

        //进行 4 路归并
        ArrayList<Integer> initValues = new ArrayList<>(sources.length);
        for (int i = 0; i < sources.length; i++) {
            initValues.add(sources[i].poll());
        }
        //初始化败者树
        LoserTree<Integer> loserTree = new LoserTree(initValues);
        //输出胜者
        Integer s = loserTree.getWinner();
        System.out.print(loserTree.getLeaf(s) + " ");
        while (sources.length > 0) {
            //新增叶子节点
            Integer newLeaf = sources[s].poll();
            if (newLeaf == null) {
                // sources[s] 对应的队列(归并段)已经为空,删除队列并调整败者树
                loserTree.del(s);
            } else {
                loserTree.add(newLeaf, s);
            }

            s = loserTree.getWinner();
            if (s == null) {
                break;
            }
            //输出胜者
            System.out.print(loserTree.getLeaf(s) + " ");
        }
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值