js-剑指offer刷题记录(链表)

1.从尾到头打印链表

输入一个链表,按链表从尾到头的顺序返回一个ArrayList

我的解法

这道题挺简单的,链表不像数组能通过下标取元素,链表只能从头开始一个一个遍历到要取的元素,通过数组方法unshift从头往数组添加元素,这样就是从尾到头顺序了。

function printListFromTailToHead(head)
{
    var array=[]
    var current=head
    while(current){
        array.unshift(current.val)
        current = current.next
    }
    return array
}

2.链表中环的入口结点

我的解法

当然是错的,从头到尾遍历结点,给结点添加一个属性,如果有环的话,那么会有某个结点指向之前被遍历过的结点,那么这个结点就有被添加的属性。不过这样做,破坏了原有的数据

其他分析

1.设置两个指针,一快一慢,快指针的速度是慢指针速度的二倍。如果有环,那么二者一定会在环中某结点相遇
2.然后一个指针从头结点出发,另一个在相遇结点出发,但是速度一致,二者再次相遇的结点就是环入口
证明
图片摘自https://www.nowcoder.com/questionTerminal/253d2c59ec3e4bc68da16833f79a38e4?f=discussion

1.指针fast每次走两步,慢指针slow每次都一步,当慢指针进入环后,就是追逐物理题了,二者肯定会相遇。
2.如上图,假设AC长度为a,CB长度为b,BC长度为c。相遇时:

  • fast指针走过的路程为a+k(b+c)+b,k>=1

  • slow指针走过的路程为a+b。

关于公式有两点说明:

  • k>=1是一定的,如果fast一环都没绕就被slow追上了,二者相同时间经过的路程一样,这与二者速度不同相悖
  • slow肯定没绕环,如果slow绕了一环,fast肯定绕了两环,早就相遇了

故有2(a+b)=a+k(b+c)+b,k>=1,从而得到a=(k-1)(b+c)+c,k>=1。a的表达式意义为:链表头到环入口的距离=相遇点到环入口的距离+(k-1)圈环长度。两个指针速度相同,在相同的时间内经过相同长度的路程,从A点出发的指针一定会和从C点出发绕环的指针在环入口处相遇。

function EntryNodeOfLoop(pHead){
    var fast = pHead
    var slow = pHead
    while(fast!=null&&fast.next!=null){
        slow = slow.next
        fast = fast.next.next
        if(slow == fast){
            let p = pHead
            while(p!=slow){
                p = p.next
                slow = slow.next
            }
            return p
        }
     }
     return null
}

一点补充

环的长度:B点在环内,新建变量记录该点位置,指针从该点出发,一边走一边计数一边判断当前点与记录,当回到记录点时,计数即为环长。
链表的长度:环长已知,再加上a的值即为链表长度了。

3.删除链表中重复的结点

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5

我的解法

错到离谱,不过也加深了对链表结构的理解,比如链表是如何删除的(谁能想到我看了一天这道题呢)

其他分析

1.怎样删除链表的某个结点?是通过改变前一个结点的next指针指向。

  • 所以本题中要删除结点,肯定要一个中间变量记录当前结点cur的前结点,这里设为pre。
  • 另外,考虑一个特殊情况那就是第一个结点与第二个结点重复,那么第一个结点怎么删?它没有前结点啊!所以要设置一个头结点,帮助我们删第一个结点

2.pre指针的作用:它是当前结点的前结点,改变它的指针来跳过重复的结点指向下一个不重复的点。那是前面的哪个结点呢?就是当前确定的不重复的点,这样当我们遍历到下一个不重复的点的时候,把pre指针指向这个点就行了。

/*function ListNode(x){
    this.val = x;
    this.next = null;
}*/
function deleteDuplication(pHead)
{
    var Head = new ListNode(0) //设置一个头结点
    Head.next = pHead  //指向第一个结点
    var pre = Head
    var cur = pHead
    while(cur != null){
        if(cur.next!=null&&cur.val == cur.next.val){
            while(cur.next!=null&&cur.val == cur.next.val){
                cur = cur.next
            }
            pre.next = cur.next
            cur = cur.next
        }else{
            pre = pre.next
            cur = cur.next
        }
    }
    return Head.next
}

3.越界问题的处理
自己一开始写的循环条件如下,直接在最外层循环判断下一结点是否为空,是的说明当前结点是最后一个结点故跳出循环。但是这样做,当链表为空的时候就会报错,所以这样做是不对的。

while(cur.next != null){
    if(cur.val == cur.next.val){
        while(cur.val == cur.next.val){

4.pre的更新:这个我想了很久,可能太蠢。
当发现接下来有重复结点的时候,我们找到重复最后一个结点然后要做的是删!所以改变pre的next指针

pre.next = cur.next

当发现下一个结点和当前结点不同时,pre是从前一个位置(这个位置上的结点是确定的不重复的点)更新到最新的不重复的点

pre = pre.next

4.复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

我的解法

我哪有解法

其他分析

三步法
剑指给出的方法很直观,画个图,思路清晰,代码其实很好写
在这里插入图片描述

function Clone(pHead)
{
    if(pHead == null) return null
    let pNode = pHead
    //第一步:复制每个结点,把新结点插入旧结点的后面
    while(pNode){
        let cloneNode = new RandomListNode(pNode.label)
        cloneNode.next = pNode.next  //注意赋值顺序
        pNode.next = cloneNode
        pNode = cloneNode.next
    }
    //第二步:旧结点的random复制
    pNode = pHead
    while(pNode){
        if(pNode.random){  //这个判断要有
            pNode.next.random = pNode.random.next
        }
        pNode = pNode.next.next 
    }
    //第三步:拆分链表
    pNode = pHead
    let cloneHead = pHead.next
    let cloneNode = cloneHead
    while(pNode){
        pNode.next = pNode.next.next
        if(cloneNode.next){
            cloneNode.next = cloneNode.next.next
        }
        pNode = pNode.next
        cloneNode = cloneNode.next
    }
    return cloneHead
}

哈希法
用一个哈希表表示新旧结点之间的映射关系:键是旧结点,值就是新结点。这里就要用到es6中新的数据结构map:特殊的对象,它的键可以是对象。

  • 第一次遍历:复制旧结点的值和next指针,用map保存新旧结点之间的映射关系
  • 第二次遍历:通过map获得旧节点对应的新节点,更新 新结点的random 指针
function Clone(pHead)
{
    if(pHead == null) return null
    
    let map = new Map()
    let pNode = pHead
    //新链表的头
    let cloneHead = new RandomListNode(pHead.label)
    let cloneNode = cloneHead
    map.set(pNode,cloneNode) //先把两个头放进去,代码会简洁一点
    //第一次遍历:复制旧结点的值和next
    while(pNode){
        //创建新结点
        if(pNode.next){  //这个判断一定要有
            cloneNode.next = new RandomListNode(pNode.next.label)
        }else{
            cloneNode.next = null
        }
        cloneNode = cloneNode.next
        pNode = pNode.next
        map.set(pNode,cloneNode)  //结点对存入map中
    }
    //第二次遍历:复制random指针
    pNode = pHead
    while(pNode){
        if(pNode.random){
            //map的get方法:由键取值
            map.get(pNode).random = map.get(pNode.random)
        }
        pNode = pNode.next
    }
    return cloneHead
}

5.二叉搜索树与双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

我的解法

结果要排序说明要用中序遍历,这样才是从小到大。遍历的同时创建结点,以及改变指针指向。所以我真的不会

其他分析

剑指有如下分析,也就是说需要记录左子树的最大结点和右子树的最小结点。这样和根结点链接,整棵树就转换好了。
这里用一个指针pre记录。
递归

  • 明确Convert函数的功能。
    输入:输入一个二叉搜索树的根节点。
    过程:将其转化为一个有序的双向链表。
    输出:返回该链表的头节点。
  • 明确成员变量last的功能。
    last用于记录当前链表的末尾节点。
  • 明确递归过程。
    递归的过程就相当于按照中序遍历,将整个树分解成了无数的小树,然后将他们分别转化成了一小段一小段的双向链表。再利用past记录总的链表的末尾,然后将这些小段链表一个接一个地加到末尾。
    在这里插入图片描述递归的代码真的头疼,一开始我的代码是这样的
function Convert(pRootOfTree)
{
    if(pRootOfTree == null) return null
    let last = null
    ConvertCore(pRootOfTree,last)
    let pHead = pRootOfTree
    while(pHead.left){
        pHead = pHead.left
    }
    return pHead
}
function ConvertCore(root,last){
    if(root == null) return 
    ConvertCore(root.left,last)
    root.left = last
    if(last) last.right = root
    last = root
    ConvertCore(root.right,last)
}

报错如下
在这里插入图片描述
树的结构如下,可知4,6,8,12四个结点都没了。
在这里插入图片描述
一直没找到出错原因,改成如下代码后正常了, 我猜测是last更新有问题。

function Convert(pRootOfTree)
{
    if(pRootOfTree == null) return null
    let last = null
    ConvertCore(pRootOfTree,last)
    let pHead = pRootOfTree
    while(pHead.left){
        pHead = pHead.left
    }
    return pHead
}
function ConvertCore(root,last){
    if(root == null) return 
    if(root.left){
        last = ConvertCore(root.left,last)
    }
    root.left = last
    if(last) last.right = root
    last = root
    if(root.right){
        last = ConvertCore(root.right,last)
    }
    return last
}

非递归
中序递归的同时,需要一个指针pre记录当前链表的末尾结点。真的比递归好理解很多。

function Convert(pRootOfTree)
{
    if (pRootOfTree == null) return null
    let stack = [];
    let node = pRootOfTree
    let pre = null
    let head = null
    while (stack.length || node) {
        //一直遍历到树的最左边也就是最小值
        if (node) {
            stack.push(node);
            node = node.left;
        } else {
            //最左子结点出栈
            let top = stack.pop()
            //pre为null说明top就是最左子结点就是链表的头
            if (pre == null) {
                head = top
            } else {  //pre不为null,那就需要top的左右指针赋值
                pre.right = top
            }
            top.left = pre
            pre = top  //链表最后结点就是top了
            node = top.right
        }
    }
    return head
}

6.两个链表的公共结点

输入两个链表,找出它们的第一个公共结点。(注意因为传入数据是链表,所以错误测试数据的提示是用其他方式显示的,保证传入数据是正确的)

我的解法

双指针,长链表的指针先走,等走到剩余长度与短链表相同时,短链表的指针也开始走,往后就要判断两指针是否相同。
很明显,要获取两个链表的长度。

function FindFirstCommonNode(pHead1, pHead2)
{
    if(pHead1 == null||pHead2 == null) 
        return null
    let gap = GetListLength(pHead1)- GetListLength(pHead2)
    let short = pHead1,long = pHead2
    //要知道哪个是长链表
    if(gap>0){
        short = pHead2
        long = pHead1
    }
    //long先走
    while(gap--){
        long = long.next
    }
    //开始一起走
    while(long!=null){
        if(long==short){
            return long
        }
        long = long.next
        short = short.next
    }
}
//获取链表长
function GetListLength(pHead){
    let cur = pHead
    let len = 0
    while(cur){
        len++
        cur = cur.next
    }
    return len
}
发布了21 篇原创文章 · 获赞 0 · 访问量 183

猜你喜欢

转载自blog.csdn.net/adrenalineiszzz/article/details/104695139
今日推荐