2023年前端面试题汇总-数据结构(链表)

news2024/11/15 17:49:56

1. 链表的概念

1.1. 链表的结构

在计算机里,不保存在连续存储空间中,而每一个元素里都保存了到下一个元素的地址的数据结构,我们称之为链表(Linked List)。链表上的每一个元素又可以称它为节点(Node),而链表中第一个元素,称它为头节点(Head Node),最后一个元素称它为尾节点(Tail Node)。

链表的结构定义中,包含了两个信息,一个是数据信息,用来存储数据的,也叫做数据域;另外一个是地址信息,用来存储下一个节点地址的,也叫做指针域。

可以看到,链表节点以整型作为数据域的类型,其中第一个链表节点,存储了 763 这个数据,指针域中呢,存储了一个 0x56432 地址,这个地址而 0x56432 正是第二个链表节点的地址。这样,第一个节点指向第二个节点,因此这两个节点之间,在逻辑上构成了一个指向关系。

在第二个节点的指针域中呢,存储了一个地址,是 0x0,这个地址值所对应就是 0。这是一个特殊的地址,我们称它为空地址,用 NULL 表示这个空地址。第二个链表节点指向空地址,就意味着它就是这个链表结构的最后一个节点。

在JavaScript中,链表的定义中包含两个属性,val 用来保存节点上的数据,next用来保存指向下一个节点的链接。使用一个构造函数来创建节点,在构造函数设置了这两个属性的值:

function ListNode(val) {
    this.val = val;
    this.next = null;
}

注意,链表结构的指针域只有一个 next 变量,这说明每一个链表节点,只能唯一地指向后续的一个节点。在JavaScript中是没有指针的概念的,所以我们可以理解这个指针是一个地址的引用。

1.2. 链表与数组对比

其实,链表结构和数组结构很类似,只不过数组结构在内存中存储是连续的,链表结构由于有指针域的存在,它的每一个节点在内存中存储的位置未必连续。下面来对比一下两者的性能。

1.2.1. 空间利用率

数组在创建之后大小是无法改变的,想要增加元素的话就必须重新创建一个新的数组。所,以有时为了能够动态地增加元素,在开始创建数组时会声明一个比需要的大小还多的空间出来,以便后面添加新的元素。这个时候就会造成空间上的浪费,所以,数组的空间利用率相当于本来需要的大小除以创建出来数组的大小。

而因为链表中的元素只有当需要的时候才会被创建出来,所以不存在需要多预留空间的情况。对于我们来说,只有节点里的值是可以利用上的,而保存节点地址的内存其实对于我们来说是无法应用的。所以链表的空间利用率上相当于值的大小除以值的大小和节点地址大小的和。

1.2.2. 时间复杂度

访问数组元素的时间复杂度是 O(1)。而因为链表顺序访问的这个特性,访问链表中第 N 个元素需要从第一个元素一直遍历到第 N 个元素,所以平均下来的时间复杂度是 O(N)。

对于数组来说,插入操作无论是发生在数组结尾还是发生在数组的中间,因为都需要重新创建一个新的数组出来,并复制一遍之前的元素到新的数组中,所以平均的时间复杂度都是 O(N)。而对于链表来说,要是一直都能维护一个尾节点的地址的话,那么插入一个新的元素只需要 O(1) 的时间复杂度。而当插入一个元素到链表中间的时候,因为链表顺序访问的这个特性,需要先遍历一遍链表,从第一个节点开始直到第 N 个位置,然后再进行插入,所以平均下来的时间复杂度是 O(N)。

1.3. 链表的形式

1.3.1. 单向链表

所有的链表节点中都只保存了指向下一个节点地址的信息。这种在一个节点中既保存了数据,也保存了指向下一个节点地址信息的链表,称之为单向链表(Singly Linked List)。如下图所示:

1.3.2. 双向链表

单向链表有着只能朝着一个方向遍历的局限性,既然可以保存指向下一个节点地址的信息,也可以保存指向上一个节点地址的信息。这种在一个节点中保存了数据也保存了连向下一个和上一个节点地址信息的链表,称之为双向链表(Doubly Linked List)。和链表中尾节点的下一个节点只保存空地址一样,链表中头节点的上一个节点地址也保存着空地址,如下图所示:

 1.3.3. 循环链表

无论是单向链表或者是双向链表,当遍历至尾节点之后就无法再遍历下去了,如果将尾节点指向下一个节点地址的信息更新成指向头节点的话,这样整个链表就形成了一个环,这种链表称之为循环链表(Circular Linked List)。如下图所示:

 

2. 链表的操作

在实现链表时候,通常在链表前面加一个假头,所谓假头,通常也叫作 Dummy Head 或者“哑头”。实际上,就是在链表前面,加上一个额外的结点。此时,存放了 N 个数据的带假头的链表,算上假头一共有 N+1 个结点。

那额外的结点不会存放有意义的数据。那么它的作用是什么呢?

其实,添加假头后,可以省略掉很多空指针的判断,链表的各种操作会变得更加简洁。关于链表的各种操作,主要是以下 6 种基本操作:

1. 链表初始化;

2. 尾部追加结点;

3. 头部插入结点;

4. 查找结点;

5. 插入指定位置之前;

6. 删除结点;

下面以 LeetCode 的707题《设计链表》为例,来实现一下单链表,题目要求将这 6 种基本的操作加以实现:注释中的 /code here/ 部分是填写相应的 6 种功能代码:

var MyLinkedList = function () {
    /* code here: 初始化链表 */
};

MyLinkedList.prototype.addAtTail = function (val) {
    /* code here: 将值为 val 的结点追加到链表尾部 */
};

MyLinkedList.prototype.addAtHead = function (val) {
    /* code here: 插入值val的新结点,使它成为链表的第一个结点 */
};

MyLinkedList.prototype.get = function (index) {
    /* code here: 获取链表中第index个结点的值。如果索引无效,则返回-1 */
    // index从0开始。
};

MyLinkedList.prototype.addAtIndex = function (index, val) {
    // code here:
    // 在链表中的第 index 个结点之前添加值为 val  的结点。
    // 1. 如果 index 等于链表的长度,则该结点将附加到链表的末尾。
    // 2. 如果 index 大于链表长度,则不会插入结点。
    // 3. 如果 index 小于0,则在头节点前插入
};

MyLinkedList.prototype.deleteAtIndex = function (index) {
    /* code here: 如果索引index有效,则删除链表中的第index个结点 */

2.1. 链表初始化

初始化假头链表,首先需要 new 出一个链表结点,并且让链表的 dummy 和 tail 指针都指向它,代码如下:

var listNode = function (val) {
    this.val = val
    this.next = null
};

var MyLinkedList = function () {
    this.dummy = new listNode()
    this.tail = this.dummy
    this.length = 0
};

初始化完成后,链表已经有了一个结点,但是此时,整个链表中还没有任何数据。因此,对于一个空链表,就是指已经初始化好的带假头链表。

虽然 head 和 tail 初始化完成之后,都指向null。但是这两者有一个特点,叫“动静结合”:

1. 静:head 指针初始化好以后,永远都是静止的,再也不会动了;

2. 动:tail 指针在链表发生变动的时候,就需要移动调整;

2.2. 尾部追加结点

尾部添加新结点操作只有两步,代码如下:

MyLinkedList.prototype.addAtTail = function (val) {
    // 尾部添加一个新结点
    this.tail.next = new listNode(val)
    // 移动tail指针
    this.tail = this.tail.next;
    // 链表长度+1
    this.length++
};

带假头的链表初始化之后,可以保证 tail 指针永远非空,因此,也就可以直接去修改 tail.next 指针,省略掉了关于 tail 指针是否为空的判断。

2.3. 头部插入结点

需要插入的新结点为 p,插入之后,新结点 p 会成为第一个有意义的数据结点。通过以下 3 步可以完成头部插入:

1. 新结点 p.next 指向 dummy.next;

2. dummy.next 指向 p;

3. 如果原来的 tail 指向 dummy,那么将 tail 指向 p;

对应的代码如下:

MyLinkedList.prototype.addAtHead = function (val) {
    // 生成一个结点,存放的值为val
    const p = new listNode(val)
    // 将p.next指向第一个结点
    p.next = this.dummy.next;
    // dummy.next指向新结点,使之变成第一个结点
    this.dummy.next = p;
    // 注意动静结合原则,添加结点时,注意修改tail指针。
    if (this.tail == this.dummy) {
        this.tail = p;
    };
    // 链表长度+1
    this.length++
};

这段代码有趣的地方在于,当链表为空的时候,它依然是可以工作的。因为虽然链表是空的,但是由于有 dummy 结点的存在,代码并不会遇到空指针。

注意: 如果链表添加了结点,或者删除了结点,一定要记得修改 tail 指针。如果忘了修改,那么就不能正确地获取链表的尾指针,从而错误地访问链表中的数据。

2.4. 查找结点

在查找索引值为 index(假设 index 从 0 开始)的结点时,你需要注意,大多数情况下,返回指定结点前面的一个结点 prev 更加有用。好处有以下两个方面:

1. 通过 prev.next 就可以访问到想要找到的结点,如果没有找到,那么 prev.next 为 null;

2. 通过 prev 可以方便完成后续操作,比如在 target 前面 insert 一个新结点,或者将 target 结点从链表中移出去;

因此,如果要实现 get 函数,应该先实现一个 getPrevNode 函数:

MyLinkedList.prototype.getPreNode = function (index) {
    if (index < 0 || index >= this.length) {
        return -1;
    }
    // 初始化front与back,分别一前一后
    let front = this.dummy.next
    let back = this.dummy
    // 在查找的时候,front与back总是一起走
    for (let i = 0; i < index && front != null; i++) {
        back = front;
        front = front.next;
    }
    // 把back做为prev并且返回
    return back
};

有了假头的帮助,这段查找代码就非常健壮了,可以处理以下 2 种情况:

1. 如果 target 在链表中不存在,此时 prev 返回链表的最后一个结点;

2. 如果为空链表(空链表指只有一个假头的链表),此时 prev 指向 dummy。也就是说,返回的 prev 指针总是有效的;

借助 getPrevNode 函数来实现 get 函数:

MyLinkedList.prototype.get = function (index) {
    // 获取链表中第 index 个结点的值。如果索引无效,则返回-1。
    // index从0开始
    if (index < 0 || index >= this.length) {
        return -1;
    }
    // 因为getPrevNode总是返回有效的结点,所以可以直接取值。
    return this.getPreNode(index).next.val
};

2.5. 插入指定位置之前

插入指定位置的前面,有 4 点需要注意。

1. 如果 index 大于链表长度,则不会插入结点;

2. 如果 index 等于链表的长度,则该结点将附加到链表的末尾;

3. 如果 index 小于 0,则在头部插入结点;

4. 否则在指定位置前面插入结点;

其中,Case 1~3 较容易处理。可以直接写。重点在于 Case 4。现在已经有了 getPrevNode() 函数,就可以比较容易地写出 Case 4 的代码,思路如下:

1. 使用 getPrevNode() 函数拿到 index 之前的结点 pre;

2. 在 pre 的后面添加一个新结点;

以下是具体的 Case 1~4 的操作过程:

MyLinkedList.prototype.addAtIndex = function (index, val) {
    if (index > this.length) {
        // Case 1 如果 index 大于链表长度,则不会插入结点。
        return;
    } else if (index == this.length) {
        // Case 2 如果 index 等于链表的长度,则该结点将附加到链表的末尾。
        this.addAtTail(val);
    } else if (index <= 0) {
        // Case 3 如果index小于0,则在头部插入结点。
        this.addAtHead(val);
    } else {
        // Case 4 得到index之前的结点pre
        const pre = this.getPreNode(index);
        // 在pre的后面添加新结点
        const p = new listNode(val);
        p.next = pre.next;
        pre.next = p;
        // 链表长度+1
        this.length++;
    }
}

2.6. 删除节点

删除结点操作是给定要删除的下标 index(下标从 0 开始),删除的情况分 2 种:

1. 如果 index 无效,那么什么也不做;

2. 如果 index 有效,那么将这个结点删除;

上面这 2 种情况中,Case 1 比较容易处理,相对要麻烦一些的是 Case 2。要删除 index 结点,最好是能找到它前面的结点。有了前面的结点,再删除后面的结点就容易多了。不过已经有了 getPrevNode 函数,所以操作起来还是很简单的。

以下是具体的操作过程:

MyLinkedList.prototype.deleteAtIndex = function (index) {
    // Case 1 如果index无效,那么什么也不做。
    if (index < 0 || index >= this.length) {
        return;
    }
    // Case 2 删除index结点
    
    // step 1 找到index前面的结点
    const pre = this.getPreNode(index);
    // step 2 如果要删除的是最后一个结点,那么需要更改tail指针
    if (this.tail == pre.next) {
        this.tail = pre;
    }
    // step 3 进行删除操作。并修改链表长度。
    pre.next = pre.next.next;
    this.length--;
};

2.7. 总结

使用哑结点来实现链表的总代码如下:

// 节点初始化
var listNode = function (val) {
    this.val = val
    this.next = null
};
// 初始化链表
var MyLinkedList = function () {
    this.dummy = new listNode()
    this.tail = this.dummy
    this.length = 0
};


// 获取上一个节点
MyLinkedList.prototype.getPreNode = function (index) {
    if (index < 0 || index >= this.length) {
        return -1;
    }
    // 初始化front与back,分别一前一后
    let front = this.dummy.next
    let back = this.dummy
    // 在查找的时候,front与back总是一起走
    for (let i = 0; i < index && front != null; i++) {
        back = front;
        front = front.next;
    }
    // 把back做为prev并且返回
    return back
};

// 获取指点节点值
MyLinkedList.prototype.get = function (index) {
    if (index < 0 || index >= this.length) {
        return -1;
    }

    return this.getPreNode(index).next.val
};


// 添加节点到链表头部
MyLinkedList.prototype.addAtHead = function (val) {
    // 生成一个结点,存放的值为val
    const p = new listNode(val)
    // 将p.next指向第一个结点
    p.next = this.dummy.next;
    // dummy.next指向新结点,使之变成第一个结点
    this.dummy.next = p;
    // 注意动静结合原则,添加结点时,注意修改tail指针。
    if (this.tail == this.dummy) {
        this.tail = p;
    }
    // 链表长度+1
    this.length++
};


// 添加节点到链表尾部
MyLinkedList.prototype.addAtTail = function (val) {
    // 尾部添加一个新结点
    this.tail.next = new listNode(val)
    // 移动tail指针
    this.tail = this.tail.next;
    // 链表长度+1
    this.length++
};

// 添加节点到指定下标
MyLinkedList.prototype.addAtIndex = function (index, val) {
    if (index > this.length) {
        // Case 1 如果 index 大于链表长度,则不会插入结点。
        return;
    } else if (index == this.length) {
        // Case 2 如果 index 等于链表的长度,则该结点将附加到链表的末尾。
        this.addAtTail(val);
    } else if (index <= 0) {
        // Case 3 如果index小于0,则在头部插入结点。
        this.addAtHead(val);
    } else {
        // Case 4 得到index之前的结点pre
        const pre = this.getPreNode(index);
        // 在pre的后面添加新结点
        const p = new listNode(val);
        p.next = pre.next;
        pre.next = p;
        // 链表长度+1
        this.length++;
    }
};

// 按指定下标删除节点
MyLinkedList.prototype.deleteAtIndex = function (index) {
    // Case 1 如果index无效,那么什么也不做。
    if (index < 0 || index >= this.length) {
        return;
    }
    // Case 2 删除index结点
    // step 1 找到index前面的结点
    const pre = this.getPreNode(index);
    // step 2 如果要删除的是最后一个结点,那么需要更改tail指针
    if (this.tail == pre.next) {
        this.tail = pre;
    }
    // step 3 进行删除操作。并修改链表长度。
    pre.next = pre.next.next;
    this.length--;
};


// 使用示例
// var obj = new MyLinkedList();
// var result = obj.get(index);
// obj.addAtHead(val);
// obj.addAtTail(val);
// obj.addAtIndex(index,val);
// obj.deleteAtIndex(index);

3. 经典题目:链表的属性

3.1. 环形链表一

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:true

解释:链表中有一个环,其尾部连接到第二个节点。

 示例 2:

输入:head = [1,2], pos = 0
输出:true

解释:链表中有一个环,其尾部连接到第一个节点。

 示例 3:

输入:head = [1], pos = -1
输出:false

解释:链表中没有环。

 进阶:你能用 O(1)(即,常量)内存解决此问题吗?

我们只需要对每个遍历过的节点进行标记(因为这里每个节点都是一个对象,所以相当于遍历这个节点时,给他设置一个flag属性,如果在此遍历到的节点存在这个属性说明形成了环),后面如果再遇到它,说明有环,就直接返回true:

var hasCycle = function (head) {
    while (head) {
        if (head.flag) {
            return true
        } else {
            head.flag = true
            head = head.next
        }
    }
    return false
};

复杂度分析:

1. 时间复杂度:O(n),其中n是链表的节点数,最差坏的情况下我们要遍历完整个链表;

2. 空间复杂度:O(n),其中n是链表的节点数,主要为哈希表的开销,最坏情况下需要将每个节点插入到哈希表中一次;

3.2. 环形链表二

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

 示例 3:

输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

进阶:你是否可以不用额外空间解决此题?

和上面一题的思路一样,都是设置一个flag,只是返回值不一样,最后指向的是有flag 的节点,所以直接返回head即可

上述方法需要开辟O(n)的储存空间来储存标记信息,那我们尝试用快慢指针来解决这个问题: 设置两个指针,快指针每次走两个节点,慢指针每次走一个节点,如果存在环,那么两个指针一定相遇。等快慢指针相遇之后,我们在用另一个指针,去寻找他们相遇的位置就可以了。

设置标识法:

// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// }


var detectCycle = function (head) {
    while (head) {
        if (head.flag) {
            return head
        } else {
            head.flag = true
            head = head.next
        }
    }
    return null
};

快慢指针法:

// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// }


var detectCycle = function(head) {
    let fast = head;
    let slow = head;
    let cur = head;
    while(fast && fast.next && fast.next.next){
        slow = slow.next
        fast = fast.next.next
        if(fast == slow){
            while(cur!=slow){
                cur = cur.next
                slow = slow.next
            }
            return slow
        }
    }
    return null
};

设置标识法复杂度分析:

1. 时间复杂度:O(n),其中n是链表的节点数,最差坏的情况下我们要遍历完整个链表;

2. 空间复杂度:O(n),其中n是链表的节点数,主要为哈希表的开销,最坏情况下需要将每个节点插入到哈希表中一次;

快慢指针法复杂度分析:

1. 时间复杂度:O(n),其中n是链表中节点数。在最初判断快慢指针是否相遇时,slow 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N);

2. 空间复杂度:O(1)。我们只使用了 slow、fast、cur 三个指针;

3.3. 相交链表

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表:

在节点 c1 开始相交。

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA= 2, skipB = 3

解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

输出:Reference of the node with value = 8

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1

解释:相交节点的值为 2(注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

输出:Reference of the node with value = 2

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB= 2

解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。 解释:这两个链表不相交,因此返回 null

输出:null

注意:

1. 如果两个链表没有交点,返回 null;

2. 在返回结果后,两个链表仍须保持原有的结构;

3. 可假定整个链表结构中没有循环;

4. 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存;

一个比较直接直接的方法就是用双指针来解决,思路就是将链表拼成ab和ba这样就消除了两者的高度差,如果a和b有相交的部分,那么ab和ba也一定有相交的部分。具体实现步骤如下:

1. 定义两个指针 pA 和 pB;

2. pA 从链表 a 的头部开始走,走完后再从链表 b 的头部开始走;

3. pB 从链表 b 的头部开始走,走完后再从链表 a 的头部开始走;

4. 如果存在相交的点就直接返回pA或者pB;

// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// };

var getIntersectionNode = function (headA, headB) {
    let pA = headA;
    let pB = headB;
    while (pA !== pB) {
        pA = pA === null ? headB : pA.next;
        pB = pB === null ? headA : pB.next;
    };
    return pA
};

复杂度分析:

1. 时间复杂度:O(m + n) ,其中m和n分别是两个链表的节点数,最差情况下需要遍历完两个链表;

2. 空间复杂度:O(1),节点指针 A , B 使用常数大小的额外空间;

3.4. 链表的中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

示例 1:

输入:[1,2,3,4,5]
输出:此列表中的中间结点为 3 
解释:我们返回了一个 ListNode 类型的对象 ans,如下所示:
ans.val = 3, 
ans.next.val = 4, 
ans.next.next.val = 5, 
ans.next.next.next = NULL.

示例 2:

输入:[1,2,3,4,5,6]
输出:此列表中的结点 4 
解释:由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点

提示: 给定链表的结点数介于 1 和 100 之间。

对于这种求链表的中间点的题,我们可以使用快慢指针来实现,初始化slow和fast两个指针,开始时两个指针都指向头结点。然后慢指针一次走一步,快指针一次走两步,这样快指针走完整个链表时,慢指针正好走到链表的中间。

在遍历的过程中,如果快指针的后一个节点为空,就结束遍历,返回慢指针的值。

// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// }

var middleNode = function (head) {
    let fast = head, slow = head;
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
    };
    return slow;
};

复杂度分析:

1. 时间复杂度:O(n),其中 n 是给定链表的结点数目;

2. 空间复杂度:O(1),只需要常数空间来存放 slow 和 fast 两个指针;

3.5. 回文链表

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false

示例 2:

输入: 1->2->2->1
输出: true

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

1. 字符串拼接

对于这道题,最直接的思路就是,遍历链表,同时正向和反向拼接链表的节点,最后比较两个拼接出来的字符串是否一样。

// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// }

var isPalindrome = function(head) {
    let a = "";
    let b = "";
    while(head){
        const nodeVal = head.val;
        a = a + nodeVal;
        b = nodeVal + b;
        head = head.next;
    };
    return a === b;
};

字符串拼接复杂度分析:

1. 时间复杂度:O(n),其中 n 指的是链表的元素个数,我们需要遍历完整个链表;

2. 空间复杂度:O(1),这里只需要常量的空间来保存两个拼接的字符串;

2. 递归遍历

1. 首先,定义一个全局的指针pointer,其初始值为head,用来正序遍历;

2. 然后,调用递归函数,对链表进行逆序遍历,当头部节点为null的时候停止遍历;

3. 如果正序遍历的节点值和逆序遍历的节点值都相等,就返回true,否则就返回false;


// function ListNode(val) {
//     this.val = val;
//     this.next = null;
// }

let pointer;
function fn(head) {
    if (!head) return true;
    const res = fn(head.next) && (pointer.val === head.val);
    pointer = pointer.next;
    return res;
}

var isPalindrome = function (head) {
    pointer = head;
    return fn(head)
};

递归遍历复杂度分析:

1. 时间复杂度:O(n),其中 n 指的是链表的大小;

2. 空间复杂度:O(n),其中 n 指的是链表的大小,最差的情况下递归栈的深度为n;

3.6. 链表组件

3.7. 链表中倒数第k个节点

4. 经典题目:链表的操作

4.1. 两数相加(1)

4.2. 两数相加(2)

4.3. 反转链表(1)

4.4. 反转链表(2)

4.5. 旋转链表

4.6. K 个一组翻转链表

4.7. 两两交换链表中的节点

4.8. 交换链表中的节点

4.9. 分隔链表(1)

4.10. 分隔链表(2)

4.11. 重排链表

4.12. 排序链表

4.13. 移除链表元素

4.14. 删除排序链表中的重复元素(1)

4.15. 删除排序链表中的重复元素(2)

4.16. 删除链表的倒数第 N 个结点

4.17. 合并两个有序链表

4.18. 合并K个升序链表

4.19. 复制带随机指针的链表

4.20. 对链表进行插入排序

4.21. 奇偶链表

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/612504.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【大数据之Hive】六、Hive之metastore服务部署

metastore为Hive CLI或Hiveserver2提供元数据访问接口。 1 metastore运行模式 metastore运行模式有两种&#xff0c;嵌入式模式和独立服务模式。 &#xff08;1&#xff09;嵌入式模式 将metastore看作一个依赖嵌入到Hiveserver2和每一个HiveCLI客户端进程&#xff0c;使得Hi…

零基础开发小程序第六课-删除数据

目录 1 物理删除数据2 逻辑删除数据总结 我们上一篇介绍了修改数据&#xff0c;本篇介绍一下删除数据。一般的小程序如果提供给管理员使用的功能&#xff0c;通常会有删除数据的功能。 删除数据有真删除和假删除的区别。那什么是真删除呢&#xff1f;真删除就是把这条数据从数据…

通过JVM深入理解Java异常机制

JVM内部结构 要深入理解JVM异常处理机制&#xff0c;需要从JVM内部结构开始。 下图描述的主要是Java程序在执行时&#xff0c;由JVM管理的运行时数据区&#xff1b;包括方法区、Java堆、Java虚拟机栈、PC寄存器、本地方法栈&#xff0c;还有常量池。它们又被分为两大类——线程…

SeaTunnel StarRocks 连接器的使用及原理介绍

作者&#xff1a;毕博&#xff0c;马蜂窝数据平台负责人&#xff0c;StarRocks 活跃贡献者 & Apache SeaTunnel 贡献者 Apache SeaTunnel&#xff08;以下简称 SeaTunnel&#xff09;是一个分布式、高性能、易扩展、用于海量数据&#xff08;离线&实时&#xff09;同步…

Spring为什么默认是单例的?

目录 一、五种作用域 二、单例bean与原型bean的区别 三、单例Bean的优势与劣势 一、五种作用域 1.singleton: singleton是Spring Bean的默认作用域&#xff0c;也就是单例模式。在整个应用程序中&#xff0c;只会创建一个实例&#xff0c;Bean的所有请求都会共享这个实例。 …

ETLCloud轻松应对CDC实时数据流和维度数据合并的需求,实时监控订单数据

如何实现实时流与批流合并打宽数据 通常情况下我们使用CDC实时监听表销售或订单表数据的LOG时会形成流式的数据&#xff0c;即订单变化时数据是按照变化时间不断的传入到ETL的流程中的&#xff0c;业务希望实时看到订单数据的报表。 CDC每次传入的数据有可能是一条也可能是多…

基于geoserver开发地图发布服务

写在前面&#xff1a;我在github上创建了对应的项目&#xff0c;可点此跳转&#xff0c;本文的所有源码均可在项目里找到&#xff0c;欢迎大家访问交流 一、开发背景 在gis领域&#xff0c;geoserver是后端地图发布的开源项目。目前我们在启动服务后&#xff0c;可通过自带的…

科研工具-R-META分析与【文献计量分析、贝叶斯、机器学习等】多技术融合实践

Meta分析是针对某一科研问题&#xff0c;根据明确的搜索策略、选择筛选文献标准、采用严格的评价方法&#xff0c;对来源不同的研究成果进行收集、合并及定量统计分析的方法&#xff0c;最早出现于“循证医学”&#xff0c;现已广泛应用于农林生态&#xff0c;资源环境等方面。…

【AIGC】14、GLIPv2 | 在 GLIP 上扩展 negative phrase 并新增分割功能

文章目录 一、背景二、方法2.1 A Unified VL Formulation and Architecture2.2 GLIPv2 pre-training2.3 将 GLIPv2 迁移到 Localization 和 VL task 三、结果3.1 One model architecture for all3.2 One set of model parameters for all3.3 GLIPv2 as a strong few-shot learn…

Latex使用algorithm2e包写伪代码

用Latex写伪代码我们需要用到一个包&#xff0c;Algorithm2e&#xff0c;这个工具包的使用手册下载地址为&#xff08;http://mlg.ulb.ac.be/files/algorithm2e.pdf&#xff09;CSDN的链接为&#xff08;&#xff09; 准备 导入该包 \usepackage[ruled,linesnumbered]{algor…

上海亚商投顾:沪指小幅震荡微涨 AI应用端持续活跃

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 市场情绪 大小指数今日走势分化&#xff0c;沪指全天窄幅震荡&#xff0c;创业板指低开低走&#xff0c;盘中一度跌超1.6%&a…

【Java基础】I/O流 —— Java中的流都需要关闭吗?

目录 一、为什么要关闭流&#xff1f;二、close方法和flush方法1.使用close方法2.使用flush方法 三、流按指向分类四、不用关闭的流 一、为什么要关闭流&#xff1f; 涉及到对外部资源的读写操作&#xff0c;包括网络、硬盘等等的I/O流&#xff0c;如果在使用完毕之后不关闭&a…

Unity基础框架从0到1(六)对象池模块

索引 这是Unity基础框架从0到1的第六篇文章&#xff0c;框架系列的项目地址是&#xff1a;https://github.com/tang-xiaolong/SimpleGameFramework 文章最后有目前框架系列的思维导图&#xff0c;前面的文章和对应的视频我一起列到这里&#xff1a; 文章 Unity基础框架从0到…

算力不竭如江海,天翼云“息壤”如何助力千行百业算力智能调度?

科技云报道原创。 数字时代下&#xff0c;算力已成为新型生产力&#xff0c;并朝着多元泛在、安全可靠、绿色低碳的方向演进。以算力为核心的数字信息基础设施&#xff0c;是国家战略性布局的关键组成部分&#xff0c;也成为数字经济时代的“大国重器”。 作为云服务国家队&am…

报表生成器FastReport .Net教程:“Text“对象、文本编辑

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表&#xff0c;同时FastReport .Net支持中文、英语等14种语言&#xff0c;可以让你的产品保证真正的国际性。 FastReport.NET官方版…

es elasticsearch 十四 各种机制 评分机制 正序索引 解决跳跃结果问题 解决耗时过长问题 解决相同属性值都到一个地方

目录 评分机制 机制 查看评分实现如何算出来的explaintrue 分析能否被搜索到 Doc value 正排序索引 Query phase Fetch phase Preference 问题 解决跳跃结果问题 Timeout 到达时间直接返回&#xff0c;解决耗时过长问题 Routing 数据准确分配到某地&#xff0c;解决相…

这才叫软件测试工程师,你那最多是混口饭吃罢了....

前些天和大学室友小聚了一下&#xff0c;喝酒喝大发了&#xff0c;谈天谈地谈人生理想&#xff0c;也谈到了我们各自的发展&#xff0c;感触颇多。曾经找工作我迷茫过、徘徊不&#xff0c;毕业那会我屡屡面试失败&#xff0c;处处碰壁&#xff1b;工作两年后我一度想要升职加薪…

006+limou+C语言“堆的实现”与“树的相关概念”

0.前言 这里是limou3434的一篇个人博文&#xff0c;感兴趣可以看看我的其他内容。本次我给您带来的是树的相关只是&#xff0c;并且把堆这一数据结构做了实现&#xff0c;后面还有大量的oj题目。但是树重点也就在这十多道oj题目中&#xff0c;您可以尝试着自己做一下&#xff…

我的创作纪念日|写在CSDN创作第512天

机缘 今天无意中发现CSDN后台给我发送私信&#xff0c;才发觉原来我的第一篇博客更新已经过去512天了&#xff0c;512天一晃而过居然还有点恍然。 作为一名网络专业的在校大学生&#xff0c;最初开始查找相关的资料其实更习惯于从外站进行查找&#xff0c;却总是在不经意中进入…

人事管理项目-前端实现

人事管理项目-前端实现 引入Element和Axios开发Login页面配置路由配置请求转发启动前端项目 引入Element和Axios 前端UI使用Element&#xff0c;网络请求则使用Axios&#xff0c;因此首先安装Element和Axios依赖&#xff0c;代码如下&#xff1a; 依赖添加成功后&#xff0c;接…