算法集训:双向链表

今天终于不是水题了!!





1.430. 扁平化多级双向链表

2.剑指 Offer II 028. 展平多级双向链表

原题链接


        你会得到一个双链表,其中包含的节点有一个下一个指针、一个前一个指针和一个额外的 子指针 。这个子指针可能指向一个单独的双向链表,也包含这些特殊的节点。这些子列表可以有一个或多个自己的子列表,以此类推,以生成如下面的示例所示的 多层数据结构 。

        给定链表的头节点 head ,将链表 扁平化 ,以便所有节点都出现在单层双链表中。让 curr 是一个带有子列表的节点。子列表中的节点应该出现在扁平化列表中的 curr 之后 和 curr.next 之前 。
返回 扁平列表的 head 。列表中的节点必须将其 所有 子指针设置为 null 。

        示例 1:

        输入:head = [1,2,3,4,5,6,null,null,null,7,8,9,10,null,null,11,12]

        输出:[1,2,3,7,8,11,12,9,10,4,5,6]

        示例 2:

        输入:head = [1,2,null,3]

        输出:[1,3,2]

        示例 3:

        输入:head = []

        输出:[]

        说明:输入中可能存在空列表。

        提示:

        节点数目不超过 1000

        1 <= Node.val <= 105

这道题还是比较简单的,如下图所示,图画的不好不要太介意…
在这里插入图片描述

对于这样一个链表,按照题目的要求展开后的序列就是:
        1   7  8  9  11  10  2  3  4  5   6

我们想要做的就是对于一个有child指针的结点,我们去吧他child指针指向的链表展开后插入到当前节点和下一个结点之间。

下面做的就简单了,在不断遍历链表的过程中,对于某个结点,如果该节点有child指针,那么我们就去搜索并将该链表展开。然后将child指针展开后链表的尾结点返回。

对于这个例子:比如1的child指针指向的链表,它指向的链表展开后就是 7  8  9  11  10,我们返回10,然后将1的next变为child,1的next也就是2的prev变为10,10的next同样也要指向2。
这一步操作的代码如下:

if(cur->child){
     clast=dfs(cur->child);
     //dfs这里先不管,只需要知道他返回的是某个链表的尾结点即可
     Node* next=cur->next;
     //双向链表的中间插入操作先记录当前节点的下一个
     cur->next=cur->child;
     //当前节点的next指向child
     cur->child=nullptr;
     //由于展开之后要求为双向链表,child指针要置空
     cur->next->prev=cur;
     //child指向链表的头结点的prev指向cur
     if(next){
         next->prev=clast;
         //如果当前结点的下一个结点不是空就将其指向clast
     }
     clast->next=next;//不管next是不是空都要将其指向next
}


接下来就是对双向链表的遍历加搜索操作,这个过程我们做的就是遍历链表并且对链表中所有结点的child进行展开,并且在展开的过程中返回当前展开链表的尾结点给上一级:

Node* dfs(Node* head){
        Node* cur=head,*last=nullptr;
        while(cur)
        {
            Node* clast;
            if(cur->child){
                clast=dfs(cur->child);
                Node* next=cur->next;
                cur->next=cur->child;
                cur->child=nullptr;
                cur->next->prev=cur;
                if(next){
                    next->prev=clast;
                }
                clast->next=next;
            }//这一部分就是对某个有child指针的结点的操作
            if(cur->next==nullptr){
               last=cur;
            }
            //这里是对last指针的维护,如果当前结点时尾结点就更新last即可
            //不用考虑他是否有child指针了
            //因为如果能走到这里,就代表之前我们已经把该链表的所有child链表都展开并插入置空了,当前链表不存在有child指针的结点了。
           cur=cur->next;
        }
        return last;
    }


完整代码如下:

class Solution {
public:
    Node* dfs(Node* head){
        Node* cur=head,*last=nullptr;
        while(cur)
        {
            Node* clast;
            if(cur->child){
                clast=dfs(cur->child);
                Node* next=cur->next;
                cur->next=cur->child;
                cur->child=nullptr;
                cur->next->prev=cur;
                if(next){
                    next->prev=clast;
                }
                clast->next=next;
            }
            if(cur->next==nullptr){
               last=cur;
            }
           cur=cur->next;
        }
        return last;
    }
    Node* flatten(Node* head) {
        if(!head){
            return nullptr;
        }//链表题一定一定要注意链表为空的情况.
        Node* last=dfs(head);
        last->next=nullptr;//对于最开始的链表我们还需要对last进行操作。
        return head;//返回头结点
    }
};

3.1472. 设计浏览器历史记录

原题链接


        你有一个只支持单个标签页的 浏览器 ,最开始你浏览的网页是 homepage ,你可以访问其他的网站 url ,也可以在浏览历史中后退 steps 步或前进 steps 步。

        请你实现 BrowserHistory 类:

        BrowserHistory(string homepage) ,用 homepage 初始化浏览器类。

        void visit(string url) 从当前页跳转访问 url 对应的页面 。执行此操作会把浏览历史前进的记录全部删除。

        string back(int steps) 在浏览历史中后退 steps 步。如果你只能在浏览历史中后退至多 x 步且 steps > x ,那么你只后退 x 步。请返回后退 至多 steps 步以后的 url 。

        string forward(int steps) 在浏览历史中前进 steps 步。如果你只能在浏览历史中前进至多 x 步且 steps > x ,那么你只前进 x 步。请返回前进 至多 steps步以后的 url 。

        示例:

        输入:

        [“BrowserHistory”,“visit”,“visit”,“visit”,“back”,“back”,“forward”,“visit”,“forward”,“back”,“back”][[“leetcode.com”],[“google.com”],[“facebook.com”],[“youtube.com”],[1],[1],[1],[“linkedin.com”],[2],[2],[7]]

        输出:

        [null,null,null,null,“facebook.com”,“google.com”,“facebook.com”,null,“linkedin.com”,“google.com”,“leetcode.com”]

        提示:

        1 <= homepage.length <= 20

        1 <= url.length <= 20

        1 <= steps <= 100

        homepage 和 url 都只包含 ‘.’ 或者小写英文字母。

        最多调用 5000 次 visit, back 和 forward 函数。


这道题仍然算是一个模拟题,不过涉及到了对链表的删除操作,这一直是比较让我头疼的问题。因为对于删除操作,很容易访问到随机位置,就连错误都不能确定具体位置。

回到本题上来,这里我们同样选择利用双向链表来存储访问历史记录,首先初始化操作没有什么要注意的点,初始化即可。

BrowserHistory(string homepage) {
        list=new Node();
        list->prev=list->next=nullptr;
        list->val=homepage;
        cur=list;//定义一个头结点和当前结点
    }


接下来是visit操作,如果当前结点没有进行前进或者后退操作我们直接在当前结点后面链接即可,如果之前执行过了back操作我们就需要先将当前结点之后的记录全部删除再将当前记录进行链接,操作如下:

void visit(string url) {
        Node* next=cur->next;//先看当前结点的下一个结点是否是空
        if(!next){
            cur->next=new Node();
            cur->next->prev=cur;
            cur->next->next=nullptr;//如果是就进行尾插
        }else {
            Node* tmp=next->next;
            next->next=nullptr;
            while(tmp){
                Node* node=tmp->next;
                delete tmp;
                tmp=node;
            }
        }//不是对之后的记录全部删除
        cur->next->val=url;
        cur=cur->next;//两种都需要进行链接都集中在这里进行了
    }


之后的forward和back操作逻辑一样,不断访问prev或者next指定的步数,如果到了某步无法再访问就结束。

string back(int steps) {
        string ans=cur->val;
        Node* pre;
        while(steps--&&cur){
        //循环进行的条件是还有步数和能够继续访问
        //不过这里我们判断能不能继续的指针是cur还需要额外记录一个prev
            pre=cur;
            cur=cur->prev;
            if(cur) ans=cur->val;
        }
        if(!cur){
            cur=pre;
        }//如果是不能访问而结束循环的情况就将cur更改为之前遍历的结点
        return ans;
    }
    
    string forward(int steps) {
        string ans=cur->val;
        Node* pre;
        while(steps--&&cur){
            pre=cur;
            cur=cur->next;
            if(cur) ans=cur->val;
        }
        if(!cur){
            cur=pre;
        }
        return ans;
    }//和之前的back同理不再阐述了


完整代码如下:

struct Node
{
    string val;
    Node* prev;
    Node* next;
};
class BrowserHistory {
    Node* list,*cur;
public:
    BrowserHistory(string homepage) {
        list=new Node();
        list->prev=list->next=nullptr;
        list->val=homepage;
        cur=list;
    }
    
    void visit(string url) {
        Node* next=cur->next;
        if(!next){
            cur->next=new Node();
            cur->next->prev=cur;
            cur->next->next=nullptr;
        }else {
            Node* tmp=next->next;
            next->next=nullptr;
            while(tmp){
                Node* node=tmp->next;
                delete tmp;
                tmp=node;
            }
        }
        cur->next->val=url;
        cur=cur->next;
    }
    
    string back(int steps) {
        string ans=cur->val;
        Node* pre;
        while(steps--&&cur){
            pre=cur;
            cur=cur->prev;
            if(cur) ans=cur->val;
        }
        if(!cur){
            cur=pre;
        }
        return ans;
    }
    
    string forward(int steps) {
        string ans=cur->val;
        Node* pre;
        while(steps--&&cur){
            pre=cur;
            cur=cur->next;
            if(cur) ans=cur->val;
        }
        if(!cur){
            cur=pre;
        }
        return ans;
    }
};

4.剑指 Offer 36. 二叉搜索树与双向链表

原题链接


        输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例
在这里插入图片描述

        我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
在这里插入图片描述

        特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。


这里要借助BST的特性来进行双向链表的构建,题目要求left是前驱,right是后继。首先定义一个dfs函数在这个函数中我们进行双向链表的构建,以及当前子树的最小结点和最大结点的存储。

先看代码:

void dfs(Node* root,Node**minnode,Node** maxnode){
        if(root==nullptr){
            *minnode=nullptr;
            *maxnode=nullptr;
            return ;
        }
        Node* lmin,*lmax,*rmin,*rmax;
        if(root->left){
            dfs(root->left,&lmin,&lmax);
            lmax->right=root;
            root->left=lmax;
            *minnode=lmin;
        }else {
            *minnode=root;
        }
        if(root->right){
            dfs(root->right,&rmin,&rmax);
            rmin->left=root;
            root->right=rmin;
            *maxnode=rmax;
        }else {
            *maxnode=root;
        }
    }


首先是递归结束的条件,如果当前结点为空,当前子树的最小和最大结点当然也是空直接返回。

接着由于bst的特性,他的左子树都比根结点小,右子树都比根结点大,我们先对左子树进行递归,找到左子树的min和max结点,然后将左子树的max和root进行链接,也就是lmax->right=root,root->left=lmax。

注意这里的root并不是左子树的root,而是当前根结点根左子树的最大结点进行链接。然后我们将当前结点的min设置为lmin。(BST的特性,左子树的最大结点也会比root小,左子树的最小结点一定是当前根结点的最小子节点)。

之后对右子树进行同样的操作,不同的是我们要链接的是root和右子树的最小结点,root->right=rmin,rmin->left=root。当前子树的最大结点也一定是右子树的最大结点。即:max=rmax.

此外如果当前结点没有左结点或者右结点的话,在该递归过程中的min或者max设置为当前结点.
借用这个例子来进行解释:
在这里插入图片描述


虽然只有三个结点不过已经可以说明对一个子树的递归过程了.

对于2,我们先去dfs左子树,传入参数lmin,lmax。这里的lmin和lmax对于左子树来说就是他的最大和和最小结点。到了1我们发现他的左子树和右子树都是空,所以先将2传过来的lmin设置为1所以将lmin和lmax都设置为1返回。

现在来到了2,将2和lmax进行链接,2的min改为左子树的最小结点也就是1。之后去搜索2的右树,同样都是空,2的rmin和rmax都是3,将2和rmin进行链接。现在我们的链表变为了 1 < - > 2 < - > 3,但是1和3还没有链接于是我们在主函数就有了下面的操作:

Node* treeToDoublyList(Node* root) {
        if(!root){
            return nullptr;//二叉树的题目也要注意空的情况
        }
        Node* minnode,*maxnode ;
        dfs(root,&minnode,&maxnode);
        maxnode->right=minnode;
        minnode->left=maxnode;//root的max和min进行链接
        return minnode;     
    }


那么这样我们的双向链表就构造成功了 < - 1 < - > 2 < - > 3 - >。

当然这种情况是最简单的构造,不过如果是更复杂的结构同样适用。因为我们在构造好了某个左子树的时候lmin就是当前整棵树的最小结点直接将min改为lmin没有任何错误,而lmax就是左子树最大但是又比root小的结点与root链接也没问题。同样的rmax就是当前整棵树的最大结点,rmin也是要与root进行链接的结点。这里自己试想一下同时拥有左右子树的情况很容易理解的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
package 单双向链表; /** * 单向链表增删改查操作 * */ public class LinkTest { public static void main(String[] args) { Link l=new Link(); l.addNode("A"); l.addNode("B"); l.addNode("C"); l.addNode("D"); l.addNode("E"); l.printNode(); System.out.println("\n是否包含D:"+l.contains("D")); System.out.println("==========删除之前的内容=========="); l.printNode(); System.out.println("\n==========删除之后的内容=========="); l.deleteNode("A"); l.printNode(); } } class Link{//链表的完成类 class Node{//保存每个节点 private String data;//节点内容 private Node next;//下一个节点 public Node(String data){ this.data=data; } public void add(Node newNode) {//将节点加入到合适的位置 if(this.next==null){ this.next=newNode; }else{ this.next.add(newNode); } } public void print() {//输出节点的内容 System.out.print(this.data+"\t"); if(this.next!=null){ this.next.print();//递归调用输出 } } public boolean search(String data){//内部搜索的方法 if(data.equals(this.data)){ return true; }else{ if(this.next!=null){//向下继续判断 return this.next.search(data); }else{ return false; } } } public void delete(Node previous, String data) { if(data.equals(this.data)){//找到了匹配的节点 previous.next=this.next;//空出当前的节点 }else{ if(this.next!=null){ this.next.delete(this, data);//继续查找 } } } } private Node root;//链表中的根节点 public void addNode(String data){//增加节点 Node newNode=new Node(data); if(root==null){ root=newNode; }else{ root.add(newNode); } } public void printNode(){//链表的输出 if(root!=null){ root.print(); } } public boolean contains(String name){//判断元素是否存在 return this.root.search(name); } public void deleteNode(String data){//链表删除节点 if(this.contains(data)){ if(this.root.data.equals(data)){//如果是根节点 this.root=this.root.next;//修改根节点 }else{ this.root.next.delete(root,data);//把下一个节点的前节点和要删除的节点内容一起传入 } } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值