基于顺序存储的二叉树结构


本文主要记录几种常见的基于顺序存储的二叉树结构的用途


1. 最大堆/最小堆;用作堆排序,优先级队列

2. 哈弗曼树; 用作编解码

3. 赢者树; 用作外部排序的多路归并

 

 

 

"++++++++++++++++++++++++++++ 最大(小)堆 +++++++++++++++++++++++++++++"

 

//【 最大(小)堆 】

// 最大堆最小堆典型的用于就是堆排序/局部排序,实现优先级队列
// 堆算法中最基本的两个例程:向上调整堆和向下调整堆,其他所有算法都是以此为实现基础

// 如果0索引位置加以利用,则父子索引关系如下:
// 对于父索引parent,其左孩子索引为2*parent+1,右孩子索引为2*parent+2;
// 对于孩子索引child,其父索引为(child-1)/2;

// 如果0索引位置留空,则父子索引关系如下:
// 对于父索引parent,其左孩子索引为2*parent,右孩子索引为2*parent+1;
// 对于孩子索引child,其父索引为child/2;

//向上调整堆(位于pos位置的元素优先级太高,需要向上调整)
template<typename T/*, typename ComparePolicy*/>
void upward_adjust_heap(T heap[], int size, int pos)
{   
    if (pos<=0 || pos>=size) return;

    T key_element = heap[pos]; //备份关键元素

    //parent作为迭代主体,child作为跟踪索引
    int child = pos, parent = (child-1)/2;
    
    //此处可以引入 ComparePolicy 代替 operator <
    while (child > 0 && heap[parent] < key_element)
    {   
        //凡是小于关键元素的值,均向下移
        heap[child] = heap[parent];

        child = parent; //迭代之前的索引备份
        parent = (child-1)/2; //向上迭代
    }

    heap[child] = key_element; //最后关键元素放置在跟踪位置
}


//向下调整堆(位于pos位置的元素优先级太低,需要向下调整)
template<typename T/*, typename ComparePolicy*/>
void downward_adjust_heap(T heap[], int size, int pos)
{
    if (pos<0 || pos > ((size-1)-1)/2) return; //处在叶子上的元素没法再向下调整

    T key_element = heap[pos]; //备份关键元素
    
    //child作为迭代主体,parent作为跟踪索引
    int parent = pos, child = 2*parent+1;
    if (child+1<size && heap[child]<heap[child+1])
    {
        child++; //修正child,使其指向最大的孩子
    }
    
    //此处可以引入 ComparePolicy 代替 operator < 
    while (child < size && heap[child] > key_element)
    {   
        //凡是大于关键元素的值,均向上移
        heap[parent] = heap[child];

        parent = child; //迭代之前的索引备份
        child = 2*parent+1; //向下迭代
        if (child+1<size && heap[child]<heap[child+1])
        {
            child++; //修正child,使其指向最大的孩子
        }
    }

    heap[parent] = key_element; //最后关键元素放置在跟踪位置
}


//向一个既有堆里加入一个元素,并且继续维持堆的结构不被破坏,堆的元素数量增加1
/
template<typename T/*, typename ComparePolicy*/>
inline void push_heap(T heap[], int size, const T& data)
{
    heap[size] = data; //首先放置在末尾
    upward_adjust_heap(heap, size+1, size); //向上调整堆(注意:此时堆的大小是size+1)
}


//从一个既有堆里弹出根元素(优先级最高),并且继续维持堆的结构不被破坏,堆的元素数量减少1
/
template<typename T/*, typename ComparePolicy*/>
inline T pop_heap(T heap[], int size)
{
    T retval = heap[0]; //先缓存根元素待以后返回

    heap[0] = heap[size-1]; //将最后一个元素移至根位置
    downward_adjust_heap(heap, size-1, 0); //向下调整堆(注意:此时堆的大小是size-1)

    return retval;
}


//使用向下调整堆方法构建堆
template<typename T/*, typename ComparePolicy*/>
void make_heap(T arr[], int size) // using downward_adjust_heap
{   
    //从最后一个元素的父元素开始直至根元素逐个向下调整
    for (int pos=((size-1)-1)/2; pos>=0; pos--)
    {
        downward_adjust_heap(arr, size, pos);
    }
}


//使用向上调整堆方法构建堆
template<typename T/*, typename ComparePolicy*/>
void make_heap2(T arr[], int size) // using upward_adjust_heap
{   
    //从第二个元素开始直至最后一个元素逐个向上调整
    for (int pos=1; pos<size; pos++)
    {
        upward_adjust_heap(arr, size, pos);
    }
}


//堆排序
template<typename T/*, typename ComparePolicy*/>
void heap_sort(T arr[], int size)
{
    make_heap(arr, size); //先构建堆

    for(int i=size; i>1; i--)
    {
        arr[i-1] = pop_heap(arr,i);
    }
}


//堆局部排序
//变体:寻找第K小的值;寻找最小的K个值
template<typename T/*, typename ComparePolicy*/>
void heap_partial_sort(T arr[], int size, int num)
{
    if (num <=0 || num > size) return;
    
    make_heap(arr, num);

    for (int i=num; i<size; i++)
    {
        if (arr[i] < arr[0]) //如果当前元素值比堆顶元素还小,则互换。
        {   
            myswap(arr[i], arr[0]);
            downward_adjust_heap(arr, num, 0); //互换了之后再调整堆
        }       
    }

    for(int i=num; i>1; i--)
    {
        arr[i-1] = pop_heap(arr,i);
    }
}

 

 

 

"++++++++++++++++++++++++++++ 哈弗曼树 +++++++++++++++++++++++++++++"

 

//【 哈弗曼树】

// 哈弗曼编码即最优二叉树,它是带权路径长度最小的二叉树。



//【哈弗曼树节点结构】
template<typename T>
struct HuffmanTreeNode
{   
    T   data;       //当前节点表示的字符,对于非叶子节点,此域无用
    int weight;     //当前节点的权值(等于左右孩子节点权值之和)
    int left;       //左孩子节点索引
    int right;      //右孩子节点索引
    int parent;     //父节点索引

    HuffmanTreeNode(const T& ch = T())
        : data(ch), weight(0), left(0), right(0), parent(0) {}
};



//【创建哈弗曼树】
HuffmanTreeNode<char> * CreateHuffmanTree(char chars[], int weight[], int n)
{   
    //n个字符所构建的哈弗曼树具有2n-1个节点,索引为0的元素留空
    HuffmanTreeNode<char> * hfmTree = new HuffmanTreeNode<char>[2*n];

    //初始化n个叶子节点
    for (int i=1; i<=n; i++)
    {   
        hfmTree[i].data = chars[i-1];
        hfmTree[i].weight = weight[i-1];
        hfmTree[i].left = hfmTree[i].right = hfmTree[i].parent = 0;
    }
    for (int i=n+1; i<2*n; i++)
    {   
        hfmTree[i].left = hfmTree[i].right = hfmTree[i].parent = 0;
    }

    //(贪心算法)依次生成n-1个非叶子节点并放置在[n+1, ..., 2n-1]
    for (int i=n+1; i<2*n; i++)
    {   
        int idx1, idx2;

        //从hfmTree[1, ..., i-1]中选择两个parent为0(树根),且权重最小的两颗树,返回俩节点索引
        select(hfmTree, i-1, idx1, idx2);

        //将这俩颗树合并成一颗树,并将父节点置于&hfmTree[i]处。
        hfmTree[i].left  = idx1;
        hfmTree[i].right = idx2;
        hfmTree[i].weight = hfmTree[idx1].weight + hfmTree[idx2].weight;

        hfmTree[idx1].parent = i;
        hfmTree[idx2].parent = i;
    }

    return hfmTree;
}

//从hfmTree[1, ..., maxidx]中选择两个parent为0(树根),且权重最小的两颗树,返回俩节点索引
void select(const HuffmanTreeNode<char> * hfmTree, int maxidx, int & outIdx1, int & outIdx2)
{   
    outIdx1 = outIdx2 = 0;

    for (int i=1; i<=maxidx; i++)
    {   
        if (hfmTree[i].parent == 0)
        {   
            if (outIdx1 == 0)
            {   
                outIdx1 = i;
                continue;
            }
            if (outIdx2 == 0 || hfmTree[i].weight < hfmTree[outIdx2].weight)
            {   
                outIdx2 = i;
                if (hfmTree[outIdx1].weight > hfmTree[outIdx2].weight)
                {   
                    std::swap(outIdx1, outIdx2);
                }
            }
        }
    }
}


//【哈弗曼编码】
void HuffmanCoding(const HuffmanTreeNode<char> hfmTree[], char * hfmCoding[], int n)
{   
    char * buff = new char[n]; //字符编码最大长度不会超过n-1
    buff[n-1] = '\0';

    //扫描哈弗曼树的每一个叶子节点
    for (int i=1; i<=n; i++)
    {   
        int index = i;
        int iStart = n-1;
        int parent = hfmTree[index].parent;
        while (parent != 0) 根据叶子节点到根节点的路径构造编码序列
        {   
            if (hfmTree[parent].left == index) //向左,则设定编码字符为'0'
            {   
                buff[--iStart] = '0';
            }
            else if(hfmTree[parent].right == index) //向右,则设定编码字符为'1'
            {
                buff[--iStart] = '1';
            }
            
            index = parent; //向上迭代
            parent = hfmTree[index].parent;
        }

        hfmCoding[i-1] = new char[n-iStart];
        strcpy(hfmCoding[i-1], buff+iStart);
    }

    delete [] buff;
}



//【哈弗曼解码】
void HuffmanDecoding(const HuffmanTreeNode<char> hfmTree[], const char *codingSeq, int n)
{
    //哈弗曼解码需要从树根开始根据编码序列还原字符
    int index = 2*n - 1; //树根
    
    while (*codingSeq != '\0')
    {   
        if (*codingSeq == '0') //如果当前编码值为'0',则进入左子树
        {   
            index = hfmTree[index].left;
            codingSeq++;
        }
        else if (*codingSeq == '1') //如果当前编码值为'1',则进入右子树
        {   
            index = hfmTree[index].right;
            codingSeq++;
        }

        //判断当前节点是否到达叶子节点
        if (hfmTree[index].left == 0 && hfmTree[index].right == 0)
        {   
            std::cout << hfmTree[index].data; // 输出当前字符
            index = 2*n - 1; //将索引重置到树根,开始另一个字符的解码           
        }
    }
}

 


 

 

"++++++++++++++++++++++++++++ 赢(败)者树 +++++++++++++++++++++++++++++"

 

//【 赢者树】嗯,这个东东貌似有很多种称谓

// 赢者树典型的用途就是用于外部排序下的多路归并。
// 赢者树与堆的区别就在于:堆中的所有节点均是独立且对等的。
// 赢者树中所有内部节点都是外部节点的副本。两者的构建方式也不同。

// K路归并算法:
// 假设有k个已经排列好的序列A1 A2 ... Ak,现想要将它们归并为一个序列,如何归并?
// 设k个序列的元素总和为n,存在时间复杂度为O(nlgk)的算法,具体如下:
// 
// 1. 从k各序列中各取出最小的一个元素组成序列B,根据B建立一个大小为k的赢者树/小顶堆(min-heap)
// 2. 取B的第一个元素(堆顶)放置在结果序列中,查看该元素的归属i(1=< i =<k)
// 3. 如果Ai不为空则取Ai的下一个元素,否则选取下一个序列Ai+1的下一个元素,将该元素放置在堆顶
// 4. 重新调整赢者树/小顶堆
// 5. 重复过程2~4直到所有序列为空
// 6. 取走堆顶元素放置在结果序列中,以B[heap-size]为新堆,顶缩小堆的大小,直到堆为空
// 
//    以上时间复杂度为:O(k) + O((n-k)lgk) + O(klgk) = O(nlgk)


//【创建赢者树】
typedef int WinnerTree;
WinnerTree * CreateWinnerTree(int players[], int num)
{   
    //num个选手所构成的赢者树具有2num-1个节点,索引为0的元素留空
    WinnerTree * winTree = new WinnerTree[2*num];

    //初始化外围叶子节点
    for (int i=num; i<2*num; i++)
    {   
        winTree[i] = players[i-num];
    }

    int * win_no_arr = new int[number];

    //初始化内部num-1个节点(反向初始化,与堆相似)
    for (int i=num-1; i>0; i--)
    {   
        int left = 2*i, right = 2*i+1;
        int winno = (winTree[left]<=winTree[right])?left:right;

        winTree[i] = winTree[winno];

        //下面这段代码主要用于在初始化内部节点的同时计算出赢者编号
        if (winno < num)
        {
            win_no_arr[i] = win_no_arr[winno];
        }
        else
        {
            win_no_arr[i] = winno;
        }
    }

    //赢者编号为:win_no_arr[1];
    delete [] win_no_arr;

    return winTree;
}



//【重新洗牌/调整赢者树(替换选手)】
int Replay(WinnerTree winTree[], int num, int newPlayer, int no)
{   
    //首先替换目标选手
    int destNo = num + no;
    winTree[destNo] = newPlayer;
    
    //开始调整赢者树
    //从变更的目标选手所对应的外部节点到根节点的路径可能都需要调整
    int parentNo = destNo/2;

    while (parentNo >= 1) //调整整条路径
    {   
        int siblingNo = (destNo%2)?(destNo-1):(destNo+1);
        int winno = (winTree[destNo]<=winTree[siblingNo])?destNo:siblingNo;
        winTree[parentNo] = winTree[winno];

        destNo = parentNo;
        parentNo /= 2;
    }
    
    return GetWinnerNo(winTree, num);
}


//【获取赢者编号】
int GetWinnerNo(const WinnerTree winTree[], int num)
{
    //(赢者树的根节点就是赢者),找出跟元素来自于哪一个外部节点
    int winno = 1; //从赢者树根部开始

    while (winno < num) // 小于num的均为内部节点
    {   
        int left = 2*winno, right = 2*winno+1;
        winno = (winTree[left]<=winTree[right])?left:right;
    }

    return winno - num;
}


//【获取赢者编号】
int GetWinnerNo(int players[], int num)
{
    //首先构建赢者树(其实在构建的同时就能得出赢者编号)
    WinnerTree * winTree = CreateWinnerTree(players, num);
    
    return GetWinnerNo(winTree, num);   
}


 

 

 - - end - -

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值