双向链表(4) - 排序二叉树转换为循环双向链表

目录

1.排序二叉树(Ordered Binary Tree)

2.循环双向链表(Circular Doubly Linked List)

3.难点

4.问题总结

5.实现概括

6.代码实现


构建一个递归函数treeToList(Node root),将一棵已排序的二叉树,调整内部指针,使之从外面看起来,是一个循环双向链表。其中前向指针存储在"small"区域,后向指针存储在"large"区域。链表需要进行调整进行升序排序,并返回链表头指针。
下面的这篇文章详细解释了这个转换的过程。
http://cslibrary.stanford.edu/109/TreeListRecursion.html

以下的绝大部分内容,都来源自这篇文章。
The Great Tree-List Recursion Problem
内容:
    1. Ordered binary tree
    2. Circular doubly linked list
    3. The Challenge
    4. Problem Statement
    5. Lessons and Solution Code
介绍:
    此问题会使用到两个数据结构:一个已排序的二叉树,以及一个循环双向链表。这两者存储的都是已排序的元素,但是看上去完全不同。

1.排序二叉树(Ordered Binary Tree)

在一棵已排序的二叉树中,每个节点包含一个独立的数据元素和指向子树的"small"以及"large"指针(有时这两个指针也称为"left"左指针和“right”右指针)。
下面所示的是一棵已排序的二叉树,包含元素1到5。

 

                         图1 - 已排序的二叉树

其中,"small"子树中的所有节点,小于等于父节点。"large"子树中的所有节点,大于父节点。所以在上述例子中,"small"子树中的节点都会小于等于4,而"large"子树中的所有节点,会大于4。这条规则对树中的每个节点都适用。
而null指针,用来表示一个分支的结束。实际上,null指针代表0个节点的树。树中最顶点的节点称为根节点root。

2.循环双向链表(Circular Doubly Linked List)

下面是1至5的一个循环双向链表

                                   图2 - 循环双向链表

循环双向链表相对普通链表,拥有下面两个额外的特性:
a)"双向"代表每个节点拥有两个指针:后向指针"next"以及前向指针"previous"。
b)"循环"代表链表没有起点和终点。实际上,最后一个节点的后向指针,指向的是首节点。而首节点的前向指针,指向的是最后一个节点。

通常,一个null指针代表一个0个元素的链表。而长度为1的链表看起来会有点奇怪。如下面所示:

 

      图3 - 长度为1的循环双向链表

长度为1的链表中的那个节点,既是首节点,也是最后一个节点(末节点),所以前向指针和后向指针,指向的都是它自己。所以即使长度为1的链表,也遵守了上面的特性。

这里就是Great Tree-List问题的本质:
二叉树中的节点,与链表中的节点,都有着相同的类型结构。它们都包含了一个元素和两个指针。唯一的区别在于,二叉树中的两个指针标识为"small"和"large",而链表中标识为"previous"和"next"。忽略掉标识后,这两个节点的类型其实是一样的。

3.难点

真正的困难在于将此二叉树转换成循环双向链表。"small"指针代表"previous",而"large"指针代表"next"。转换完成后,需要调整链表来达到升序排序。

 

                      图4 - 添加了后向指针(next)后的二叉树

上面的这个图中,黑色线条表示原始二叉树中的后向指针,即链表中用箭头表示的。前向指针没有进行演示。

完整的调整演示:

 

                  图5 - 添加了前向指针与后向指针的二叉树

上面的这个图,演示了这个问题的所有情况。原始二叉树使用黑色线条,并且给二叉树添加了后向/前向指针。注意头指针的位置,它的前向指针与后向指针,构成了一个包含元素1~5的链表,如图2所示。尽管这两幅图中每个节点有着不同的空间分配,但这只是它们的数据结构属性的差异。指针结构才是关键。

4.问题总结

构建一个递归函数treeToList(Node root),将一棵已排序的二叉树,调整内部指针,使之从外面看起来,是一个循环双向链表。
其中前向指针存储在"small"区域,后向指针存储在"large"区域。链表需要进行调整进行升序排序,并返回链表头指针。
操作可以在O(n)时间完成 - 因为本质上只需在每个节点上操作一次。通常将图1做为输入,调整指针后,生成图2。

5.实现概括

a)递归是关键。对每颗子树进行递归,并合并每次递归的返回结果。
b)递归会逐步把small和large子树,调整为链表。然后把所有的这些链表合并成一个完整链表。单独定义一个函数append(Node a, Node b)来接收两个链表的输入,然后返回合并后的链表。独立出来的这个函数,也会减小递归函数中的逻辑复杂度。

6.代码实现

#include <iostream>

struct Node {
    int data;
    struct Node* small;
    struct Node* large;
};

//辅助函数-合并两个节点。
void joinNode(Node* a, Node* b) {
    /*
    a
     \
      \
       b
    */
    a->large = b;
    b->small = a;
}

//辅助函数 - 合并两个循环双向链表
Node* appendList(Node* a, Node* b) {
    Node* aLast, * bLast;
    if (a == NULL)
        return b;
    if (b == NULL)
        return a;

    aLast = a->small;
    bLast = b->small;

    joinNode(aLast, b);
    joinNode(bLast, a);

    return a;
}

//递归函数
//将一棵已排序二叉树转换为一个循环双向链表,并返回链表。
Node* treeToList(Node* root) {
    Node* aList, * bList;

    if (root == NULL)
        return NULL;

    aList = treeToList(root->small);
    bList = treeToList(root->large);

    //构造一个长度为length-1的链表
    root->small = root;
    root->large = root;

    aList = appendList(aList, root);
    aList = appendList(aList, bList);

    return aList;
}

Node* createNode(int data) {
    Node* node = new Node;
    node->data = data;
    node->small = NULL;
    node->large = NULL;
    return node;
}

void insertNode(Node** rootRef, int data) {
    Node* root = *rootRef;
    if (root == NULL)
        *rootRef = createNode(data);
    else {
        if (data <= root->data)
            insertNode(&(root->small), data);
        else
            insertNode(&(root->large), data);
    }
}

void printList(const Node* head) {
    const Node* current = head;
    while (current != NULL) {
        std::cout << current->data << " ";
        current = current->large;
        if (current == head)   //已完成了一次链表遍历
            break;
    }
    std::cout << std::endl;
}

int main() {
    Node* root = NULL, * head = NULL;

    //4-->2-->1-->3-->5
    insertNode(&root, 4);
    insertNode(&root, 2);
    insertNode(&root, 1);
    insertNode(&root, 3);
    insertNode(&root, 5);

    head = treeToList(root);

    printList(head);

    return 0;
}

运行结果:
1 2 3 4 5

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值