【算法初探】前端学算法之反转单向链表

107 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 10 天,点击查看活动详情

前因

前面的文章中,我们介绍了队列,今天我们继续来学习一个新的数据结构 -- 链表链表是什么呢?估计有一些不是专业计算机毕业的童鞋可能不清楚,如果不了解链表是什么的童鞋,可以点击这里进行查看,大致来说链表是一种物理结构(非逻辑结构),类似于数组。每种语言中都有链表,只是js中没有,因此需要自己来实现。

因为数组本身是一个连续性的数据结构,如果我们要将数组中最后一项拿出来再插入到数字的第一项,其实是非常耗时的操作;而链表本身是一种比较零散的数据结构,它可以随意的改变某个值的下一个值的指向,因此操作起来效率会比数组高很多。

链表就简单的说这么多,今天我们就一起来看一个leetcode上关于链表的题目 -- 反转链表,具体的题目描述在这里。如下图所示:

image.png

链表实现

在上面我们分析了链表是什么,以及我们要解答的题目,接下来我们先来实现链表,代码如下:

interface ILinkListNode {
    value: number;
    next?: ILinkListNode;
}

/*
 * 根据数组创建单向链表
 * @param arr number array
 */
function createLinkList(arr: number[]): ILinkListNode {
    const len = arr.length;
    // 如果这个数组的长度为空,则直接抛出错误
    if (len === 0) throw new Error('arr is empty');
    
    // 定义当前链表节点
    let curNode: ILinkListNode = {
        // 因为链表是从前往后访问的,所以我们需要获取到最后一个链表的值,即数组中最后一个值
        value: arr[len - 1],
    }
    // 如果这个数组的长度只有一位,则直接返回当前链表
    if (len === 1) return curNode;
    
    // 例如这个数组长度是3,[3, 4, 5]
    // 第一次拿到最后的值 { value: 5 }
    // 循环遍历后第二次的值 { value: 4, next: { value: 5 } }
    // 循环遍历后第三次的值 { value: 3, next: { value: 4, next: { value: 5 } } }
    
    // 循环遍历这个数组,生成每个链表的节点
    for (let i = len - 2; i >= 0; i--) {
        curNode = {
            value: arr[i],
            next: curNode
        }
    }
    
    return curNode;
}

// 测试代码
const arr = [100, 200, 300, 400, 500];
console.log(createLinkList(arr));

上述的代码中,我们通过数组来实现单向链表链表中通过next来将它们连接在一起,我们可以在TypeScript官网的Playground中测试一下该方法是否能够正常运行,运行的结果如下图所示:

image.png

具体的测试效果可以狠戳这里查看。

链表 vs 数组

数组是一种连续性存储的数据结构,而链表是一种零散存储的数据结构,但是它们都是有序的数据结构,意思是它们的两都是按顺序进行排列的,顺序是不能乱的。而对象object则是一种无序的数据结构,它是可以随意的更改元素的顺序的。

链表的查询很慢,时间复杂度是O(n),但是它的新增和删除很快,时间复杂度是O(1)数组的查询很快,时间复杂度是O(1),但是它的新增和删除很慢,时间复杂度是O(n)

链表在前端中的应用,举例来说,我们使用的React中就用到了链表这个数据结构,React Fibel中通过链表DOM树连接在一起,以此来实现多片渲染,大大加快我们的界面展示。

解题思路

在前面我们实现了单向链表这个题目,我们通过next指向前一个节点这样的操作来进行反转,但是这样很容易造成nextNode丢失,当某个节点的next断了,就很容易造成节点的丢失。因此我们需要借助第三个变量来存储中间的链表节点,才能实现最终的反转单向链表,接下来一起看一下具体的代码实现,如下:

...otehr code

/*
 * 反转单向链表,并返回反转后的 head node
 * @param listNode 单向链表 list
 */
function reverseLinkList(listNode: ILinkListNode): ILinkListNode {
    // 定义三个指针
    let prevNode: ILinkListNode | undefined = undefined;
    let curNode: ILinkListNode | undefined = undefined;
    let nextNode: ILinkListNode | undefined = listNode;
    
    // 以nextNode为主,遍历链表
    while (nextNode) {
        // 当第一个元素存在时,它本身是没有下一个指向的值,所以需要删除next,防止循环引用
        if (curNode && !prevNode) {
            delete curNode?.next;
        }
        
        // 反转指针
        if (curNode && prevNode) {
           curNode.next = prevNode;
        }
        
        // 整体移动指针的指向
        prevNode = curNode;
        curNode = nextNode;
        nextNode = nextNode?.next;
    }
    
    // 当nextNode为空时,curNode还没有设置next的执行,因此需求设置为上一个
    curNode!.next = prevNode;
    
    return curNode!;
}

...other code

// 代码测试 - 反转链表
const list1 = reverseLinkList(list);
console.log('反转链表:', list1);

上述的代码中,我们通过数组来实现单向链表反转,我们可以在TypeScript官网的Playground中测试一下该方法是否能够正常运行,运行的结果如下图所示:

image.png

具体的测试效果可以狠戳这里

最后

我们总结一下这一节的内容。

链表零散存储的有序数据结构,是非常重要的一种数据结构,在日常的算法中会经常用到链表这种数据结构,而要实现链表反转,则需要借助第三个节点,才能完成反转,并且链表的反转比较繁琐,因此需要自己多写多练才能理解。

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

【算法初探】前端学算法之旋转数组(1)

【算法初探】前端学算法之旋转数组(2)

【算法初探】前端学算法之有效的括号

【算法初探】前端学算法之用栈实现队列