前端链表面试题

链表

链表存储有序的元素集合,但链表中的元素在内存中并不是连续的,每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针)组成,如下图所示:

链表.png

相对于数组,链表的好处在于,添加或移除元素时不需要移动其他元素。但是若想要访问链表中的元素,就必须从链表起点开始查找,而不能像数组通过索引下标来查找元素。

//创建一个链表,并为其添加一些方法——来自于《学习JavaScript数据结构与算法》
function LinkedList(){
    //定义一个节点
    var Node=function(element){
        this.element=element;
        this.next=null;
    };

    var length=0;
    var head=null;

    //向链表尾部追加元素,链表尾部元素的下一个元素始终为null
    this.append=function(element){
        //node元素被创建时,它的next指针始终为null
        var node=new Node(element);
        var current;

        //当head为null,说明是一个空链表
        if(head===null){
            head=node;
        }else{
            //链表不为空,因此需要找到尾部元素,尾部元素的下一个结点为null,因此需要找到下一个结点为null的元素
            current=head;

            //循环列表,直到找到最后一项
            while(current.next){
                current=current.next;
            }
            //找到最后一项,将其next赋值为node,建立链接
            current.next=node;
        }
        length++;   //更新链表长度
    };

    //向链表中的任意位置插入一个新的项
    this.insert=function(position,element){
        if(position >= 0 && position <= length){
            var node = new Node(element);
            var current = head, previous;
            var index = 0;

            if(position === 0){  //z在第一个位置添加
                node.next = current;
                head = node;
            }else{
                while(index++ < position){
                    previous = current;
                    current = current.next;
                }
                node.next = current;
                previous.next = node;
            }
        }else{
            return false;
        }
    };

    //从链表的特定位置移除一项
    this.removeAt=function(position){
        if(position>-1 && position<length){
            var current=head;
            var previous;
            var index=0;

            //移除第一项
            if(position===0){
                head=current.next;    //current.next=null
            }else{
                while(index++ <position){
                    previous=current;
                    current=current.next;
                }
                //将previous与current的下一项连接起来,跳过current,从而移除它
                previous.next=current.next;
            }
            length--;
            return current.element;
        }else{
            return null;
        }
    };

    //从链表中移除一项
    this.remove=function(element){
        var index = this.indexOf(element);
        return this.removeAt(index);
    }; 

    //返回元素在链表中的索引,不存在则返回-1
    this.indexOf=function(element){
        var current = head;
        var index = -1;
        while(current){
            if(element === current.element){
                return index;
            }
            index++;
            current = current.next;
        }
        return -1;
    };  

    //判断链表是否为空,为空返回true,有值返回false
    this.isEmpty=function(){
        return length === 0;
    }; 

    //返回链表包含的元素个数,与length属性类似
    this.size=function(){
        return length;
    };  

    this.getHead = function(){
        return head;
    }
    
    //将LinkedList对象转换成一个字符串
    this.toString=function(){
        var current = head;
        var string =  '';
        while(current){
            string += current.element;
            current = current.next;
        }
        return string;
    };

    //函数的打印,将其转换为一个数组输出,与toString方法类似
    this.print=function(){
        var current = head;
        var array = [];
        while(current){
            array.push(current.element);
            current = current.next;
        }
        return array;
    };
}

常见链表题

从尾到头打印链表值

输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)——《剑指offer》题06

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
//用一个队列来存储打印结果,从队列头部插入
var reversePrint = function(head) {
    let array = [];
    while(head){
        array.unshift(head.val);
        head = head.next;
    }
    return array;
};
//也可以利用栈的特点,将节点值存入到栈中,然后进行反转
var reversePrint = function(head){
    let array = [];
    while(head){
        array.push(head.val);
        head = head.next;
    }
    return array.reverse();
}

反转链表

反转一个单链表,可以使用迭代或递归—《剑指offer》题24

GIF.gif

IMG_20200428_181739.jpg

//迭代写法
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
var reverseList = function(head){
    if(head == null || head.next == null){return head;}
    let pre = head;
    let temp = null;
    let cur = head.next
    while(cur != null){
        temp = cur.next;
        pre.next = cur.next;
        cur.next = head;
        head = cur;
        cur = temp;
        //[head, cur] = [cur, temp]; //解构赋值写法
    }
    return head;
}

递归写法图解

IMG_20200428_175619.jpg

//递归写法
var reverseList = function(head){
    return reverse(null, head);
}
function reverse(pre, cur){
    if(cur == null){return pre};
    let temp = cur.next;
    cur.next = pre;
    return reverse(cur, temp);
}

合并两个排序的链表

输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的——《剑指offer》题25

//迭代写法
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
var mergeTwoList = function(l1, l2){
 	if(!l1){return l2;}
    if(!l2){return l1;}
    let temp = new ListNode();
    let head = temp;  //保存头节点
    while(l1 && l2){
        if(l1.val < l2.val){
            temp.next = l1;
            l1 = l1.next;
        }else{
            temp.next = l2;
            l2 = l2.next;
        }
        temp = temp.next;
    }
    //当l1或l2为空时,跳出while循环,并将不为空的链表添加到temp.next
    if(!l1){temp.next = l2;}
    if(!l2){temp.next = l1;}
    return head.next;  //由于head头节点没有值,因此是返回head.next
}
//递归写法
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
var mergeTwoList = function(l1, l2){
	if(!l1){return l2;}
    if(!l2){return l1;}
    if(l1.val < l2.val){
        l1.next = mergeTwoList(l1.next, l2);
        return l1;
    }else{
        l2.next = mergeTwoList(l1, l2.next);
        return l2;
    }
}

链表倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点—《剑指offer》题22

//自己的写法,比较麻烦:先得到链表的长度,然后再得到顺数第len-k个节点,即为所求节点
var getKthFromEnd = function(head, k) {
    if(!head || !k){return null;}
    let len = 0;
    let temp = head;
    while(temp){
        temp = temp.next;
        len++;
    }
    let pos = len - k;
    let index = 0;
    while(index != pos){
        head = head.next;
        index++;
    }
    return head;
};
//参考别人的写法:用两个指针,一个先走k步,然后两个指针一块走,当前一个指针到达终点时,后一个节点所在位置即为所求
var getKthFromEnd = function(head, k) {
    if(!head || !k){return null;}
    let font = head, behind = head;
    let index = 0;
    while(font){
        if(index >= k){
            behind = behind.next;
        }
        font = font.next;
        index++;
    }
    return index < k ? null : behind;
}

链表中环的入口节点

给定一个链表,若其中包含环,请找出该链表的环的入口节点,否则输出null—《剑指offer》题23

思路:

  1. 判断链表是否有环:设置两个指针p1,p2,从头节点出发,p1走两步,p2走一步,如果能相遇,说明链表有环;
  2. 从环内的某个节点开始计数,再回到此节点时得到链表环的长度len;
  3. p1,p2回到头节点,让p1走len步,当p2与p1相遇时即为链表环的起点
function EntryNodeOfLoop(head){
    if(!head || !head.next){return null;}
    let p1 = head.next, p2 = head.next.next;
    //判断是否有环,无环返回null,有环则p1 == p2
    while(p1 != p2){
        if(!p2 || !p2.next){return null;}
        p1 = p1.next;
        p2 = p2.next.next;
    }
    //获取环的长度len,p1已在环内
    let temp = p1;
    let len = 1;
    p1 = p1.next;
    while(p1 != temp){
        p1 = p1.next;
        len++;
    }
    //找到入口节点
    p1 = p2 = head;
    while(len-- > 0){
        //p2先走len个长度
        p2 = p2.next;
    }
    while(p1 != p2){
        p1 = p1.next;
        p2 = p2.next;
    }
    return p1;
}

两个链表的第一个公共节点

输入两个链表,找出它们的第一个公共节点—《剑指offer》题52

思路:

  1. 先找到两个链表的长度len1,len2
  2. 让长一点的链表先走len2-len1步,使得两个链表距离链表尾部的长度相同;
  3. 两个链表一起走,并比较节点是否相同,相同即为公共节点,判断是否相同是需要值相同,并下一个节点也相同
var getIntersectionNode = function(headA, headB) {
    if(!headA || !headB){return null;}
    let len1 = getLength(headA);
    let len2 = getLength(headB);
    let long, short, gap;
    //判断哪个链表长度更长
    if(len1 > len2){
        long = headA;
        short = headB;
        gap = len1 - len2;
    }else{
        long = headB;
        short = headA;
        gap = len2 - len1;
    }
    //让长链表先走gap步
    while(gap-- > 0){
        long = long.next;
    }
    //两个链表一起走,判断第一个公共节点
    while(long){
        if(long === short){
            return long;
        }
        long = long.next;
        short = short.next;
    }
    return null;
};
function getLength(head){
    let cur = head;
    let len = 0;
    while(cur){
        len++;
        cur = cur.next;
    }
    return len;
}
//另一种解法-先让p1在headA中走完,然后跳转到headB头部;同时p2在headB中走,走完跳转到headA的头部,当p1与P2相等时,就是公共节点
var getIntersectionNode = function(headA, headB) {
    let p1 = headA, p2 = headB;
    while(p1 || p2){
        if(p1 === p2){
            return p1;
        }
        if(p1){
            p1 = p1.next;
        }else{
            p1 = headB;
        }
        if(p2){
            p2 = p2.next;
        }else{
            p2 = headA;
        }
    }
    return null;
}

圈圈中最后剩下的数字-(约瑟夫环)

0,1,...,n-1n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。 —《剑指offer》题62

//数组方法
var lastRemaining = function(n, m) {
    if(n < 1 || m < 1){return -1;}
    let array = [];
    let index = 0;
    for(let i = 0; i < n; i++){
        array[i] = i;
    }
    while(array.length > 1){
        //计算每次删除元素的下标
        index = (index + m) % array.length - 1;
        if(index >= 0){
            array.splice(index, 1);
        }else{
            array.splice(array.length - 1, 1);
            index = 0;
        }
    }
    return array[0];
}
//用链表来模拟环
//记录头节点的前一个节点cur,以保证找到要删除的节点是cur.next
//每次循环m次找到目标节点删除,直到链表只剩下一个节点
//该方法会超时
var lastRemaining = function(n, m) {
    if(n < 1 || m < 1){
        return -1;
    }
    const head = {val:0};
    let cur = head;
    for(let i = 1; i < n; i++){
        cur.next = {val:i};
        cur = cur.next;
    }
    cur.next = head; //构成环
    
    while(cur.next != cur){
        for(let i = 0;i < m-1; i++){
            cur = cur.next;
        }
        cur.next = cur.next.next;
    }
    return cur.val;
}
//数学的递归写法
//推导公式:f(n,m) = (f(n-1,m)+m)%n
//该方法爆栈
var lastRemaining = function(n, m) {
    if(n < 1 || m < 1){
        return -1;
    }else{
        return mathRecursion(n,m);
    }
};
function mathRecursion(n,m){
    if(n === 1){
        return 0;
    }
    return (mathRecursion(n-1, m)+m)%n;
}
//迭代写法
var lastRemaining = function(n, m) {
    let result = 0;
    for(let i = 2;i <= n;i++){
        result = (m + result) % i;
    }
    return result;
}

删除链表中的节点

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。

返回删除后的链表的头节点。—《剑指offer》题18

//时间复杂度为O(n)
var deleteNode = function(head, val) {
    if(!head){return null;}
    let pre = new ListNode();
    pre.next = head;
    let cur = pre;
    while(cur.next){
        if(cur.next.val === val){
            cur.next = cur.next.next;
            break;
        }else{
            cur = cur.next;
        }
    }
    return pre.next;
}

删除表中重复的节点

移除未排序链表中的重复节点。保留最开始出现的节点 。

思路:利用数组的下标来存储节点的值,并将元素值设置为1,然后当遇到相同的值时,就会重复索引,再将该节点删除

var removeDuplicateNodes = function(head) {
    let arr = [];
    let pre = null;
    let cur = head;
    while(cur != null){
        if(arr[cur.val] != undefined){
            pre.next = cur.next;
            cur = cur.next;
        }else{
            arr[cur.val] = 1;
            pre = cur;
            cur = cur.next;
        }
    }
    return head;
}

复杂链表的复制

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head —《剑指offer》题35

思路:

  1. 第一次循环复制每个节点和next指针,并保存为原节点-复制节点的映射关系
  2. 第二次循环,通过哈希表获得节点对应的复制节点,更新random指针
/**
 * // Definition for a Node.
 * function Node(val, next, random) {
 *    this.val = val;
 *    this.next = next;
 *    this.random = random;
 * };
 */
var copyRandomList = function(head) {
    if(!head){return null;}
    const map = new Map();
    let cur = head; //当前节点
    const newHead = new Node(cur.val);
    let newCur = new newHead; //当前节点的复制节点
    map.set(cur, newCur);
    
    //第一次循环,复制每个节点的值和指针
    while(cur.next){
        newCur.next = new Node(cur.next.val);
        cur = cur.next;
        newCur = newCur.next;
        map.set(cur, newCur);
    }
    //第二次循环
    newCur = newHead;
    cur = head;
    while(newCur){
        newCur.random = map.set(cur.random);
        newCur = newCur.next;
        cur = cur.next;
    }
    return newCur;
}

猜你喜欢

转载自blog.csdn.net/qq_31947477/article/details/105862374
今日推荐