什么是链表?

写在前边

对于数组的学习呢,在前端中,最重要的就是数组的一些方法的使用,数组的截取、查找、反转等常用的方法;除此之外,数组另一个稍微难点就是算法题。上一周我又拿起《剑指offer》把关于数组的算法题刷了一遍,又总结了很多的做题技巧和有关数组的解题思路,后期会分享。

那对于链表呢,我们在项目中用到的不如数组频繁,但是面试是个重点,为什么面试官喜欢考我们链表呢?想必大家对这个问题很感兴趣,因为链表灵活、涉及到的边界条件多,又加上很多细节点,对应聘者是一个考验。

不料,暑假参加的第一个公司的就让我手写一个双向链表,并完成插入数据和删除数据的操作。当时我很蒙蔽,懵逼的不是思路,而是手写,虽然写出来了,但是很多边界条件和代码规范自我感觉不好,所以有了这些细心的总结。那么今天的主题就是徒手写链表,应聘者该如何下手?

我们通常写链表准备应聘的时候,通常背加上理解,但是过了几天又让你写。就会陌生了,虽然有点思路。还是模模糊糊,小鹿也有这个记性的“毛病”,“有毛病”就要治,怎么治?我们必须在脑海里形成一套可行的步骤和方法,在遇到手写就不用手忙脚乱,而是稳稳当当,从头到尾写出一个漂亮的链表结构及操作。

思维导图

image-20231124153352083

1 熟悉结构

首先我们要知道链表的结构以及每个节点的结构,这是我们手写链表的第一步,也是学习链表的第一步。我们知道,每个链表时这样表示的:

image-20231124153430699

那每个节点结构是由数据域指针域组成,数据域是存放数据的,而指针域存放下一结点的地址。

image-20231124153502391

我们可以通过数据域访问到我们要的数据,而通过指针域访问到当前结点以后的结点,那么将这些结点串起来,就是一个链表。

那么用代码怎么来表示呢?

我们通常会用到函数,我们如果将一个函数抽象成一个结点,那么我们给函数添加两个属性,一个属性是存放数据的属性data,另一个属性是存放指向下一个结点的指针属性next

你可能会问,如果我们有成千上万个结点,要定义成千上万个函数?有一个函数叫做构造函数,想必大家都听说过,声明一个构造函数就可以创造出成千上万个结点实例。

1function Node(data){
2    this.data = data;
3    this.next = null;
4}

还有一个方法就是使用类的概念,在JavaScript中,类的概念在ES6才出现,使用 Class 来声明一个类,我们可以为类添加两个属性,同上,一样可以创造出多个结点实例。

1class Node{
2    constructor(data){
3        this.data = data;
4        this.next = null;
5    }
6}

2理清思路

如果你把链表的结构搞得明明白白了,恭喜你,成功的进入第二关,但是你只超越了百分之20的人,继续加油。

既然链表的结构弄明白了,那么我们开始理思路,我们就先拿最简单的单链表开刀,我们要完成两个操作,插入数据和删除数据。

如果我想插入数据,你可能会问,往哪里插呢?有几种插入的方法?

开始想,插入到单链表的头部算一种。

然后插入到单链表的中间算一种。

插入到单链表尾部又算一种。

所有可能的情况就三种。那么删除呢?想必你也想到了,也一共三种,删除头部、删除中间部分、删除尾部。

如果你觉的现在可以写代码了,那你就错了,虽然我们的思路非常清晰,但是面试官仅仅考我们思路吗?其实这一关你只打败了百分之50%的人,最重点、最主要的是在下一个部分,边界条件。

3 边界条件

边界条件是这五个步骤最容易犯错的一部分,因为通常考虑的不全面,导致了最后的面试未通过。如果想做好这一部分,也不难,跟着小鹿的方法走。

3.1 输入边界

首先我们先考虑用户输入的参数,比如传入一个链表,我们首先要判断链表是否为空,如果为空我们就不能让它执行下边的程序。再比如插入一个结点到指定结点的后边,那么你也要判断输入的结点是否为空,而且还要判断该结点是否存在该链表中。对于这些输入值的判断,小鹿给他同一起个名字叫做输入边界。

3.2 特殊边界

特殊边界考虑到一些特殊情况,比如插入数据,我们插入数据一般考虑到插入尾部,但是突然面试官插入到头部,插入尾部的代码并不适用于插入到头部,所以呢需要考虑这种情况,删除节点也是同样思考。其实特殊边界最主要考虑到一些逻辑上的特殊情况,考察应聘者的考虑的是否全面。小鹿给他起个名字叫做特殊边界。

4 手写代码

4.1 定义结点

image-20231124183525481

1class Node{
2    constructor(data){
3        this.data = data;
4        this.next = null;
5    }
6}

4.2 增加结点

咱们就以单链表中部添加数据为例子,分解成每个步骤,每个步骤对应代码如下:

1、保存临时地址(4结点的地址),需要进行遍历查找到3结点,也就是下列代码的currentNode 结点。

1//先查找该元素
2let currentNode = this.findByValue(element);
3// 保存 3 结点的下一结点地址(4 结点的地址)
4let pre = currentNode.next

2、创建新结点,将新结点(5结点)的指针指向下一结点指针(4结点地址,已经在上一步骤保存下来了)

3、3 的结点地址指向新结点(5结点)

1 currentNode.next = newNode;

以上步骤分析完毕,剩下的两个在头部插入和在尾部插入同样的分析方式,将这两个作为练习题,课下自己试一试这个步骤。

4.3 删除节点

删除节点也分为三种,头部、中部、尾部,咱们就删除中间结点为例进行删除,我们详细看步骤操作。

我们先看删除的全部动画,然后再分步拆分。

断开3结点的指针(断开3结点相当于让2结点直接指向4结点)

 1 let currentNode = this.head;
 2 // 用来记录 3 结点的前一结点
 3 let preNode = null;
 4 // 遍历查找 3 结点
 5 while(currentNode !== null && currentNode.data !== value){
 6        // 3 结点的前一结点
 7        preNode = currentNode;
 8        // 3 结点
 9        currentNode = currentNode.next;
10}

让结点2的指针指向4结点,完成删除。

1preNode.next = currentNode.next;

剩下的删除头结点和删除尾结点同样的步骤,自己动手尝试下。

所有代码实现:

  1/**
  2 * 2019/3/23
  3 * 公众号:「一个不甘平凡的码农」
  4 * @author 小鹿
  5 * 功能:单链表的插入、删除、查找
  6 * 【插入】:插入到指定元素后方
  7 * 1、查找该元素是否存在?
  8 * 2、没有找到返回 -1
  9 * 3、找到进行创建结点并插入链表。
 10 * 
 11 * 【查找】:按值查找/按索引查找
 12 * 1、判断当前结点是否等于null,且是否等于给定值?
 13 * 2、判断是否可以找到该值?
 14 * 3、没有找到返回 -1;
 15 * 4、找到该值返回结点;
 16 * 
 17 * 【删除】:按值删除
 18 * 1、判断是否找到该值?
 19 * 2、找到记录前结点,进行删除;
 20 * 3、找不到直接返回-1;
 21 */
 22//定义结点
 23class Node{
 24    constructor(data){
 25        this.data = data;
 26        this.next = null;
 27    }
 28}
 29
 30//定义链表
 31class LinkList{
 32    constructor(){
 33        //初始化头结点
 34        this.head = new Node('head');
 35    }
 36
 37    //根据 value 查找结点
 38    findByValue = (value) =>{
 39        let currentNode = this.head;
 40        while(currentNode !== null && currentNode.data !== value){
 41            currentNode = currentNode.next;
 42        }
 43        //判断该结点是否找到
 44        console.log(currentNode)
 45        return currentNode === null ? -1 : currentNode;
 46    }
 47
 48    //根据 index 查找结点
 49    findByIndex = (index) =>{
 50        let pos = 0;
 51        let currentNode = this.head;
 52        while(currentNode !== null && pos !== index){
 53            currentNode = currentNode.next;
 54            pos++;
 55        }
 56        //判断是否找到该索引
 57        console.log(currentNode)
 58        return currentNode === null ? -1 : currentNode;
 59    }
 60
 61    //插入元素(指定元素向后插入)
 62    insert = (value,element) =>{
 63        //先查找该元素
 64        let currentNode = this.findByValue(element);
 65        //如果没有找到
 66        if(currentNode == -1){
 67            console.log("未找到插入位置!")
 68            return;
 69        }
 70        let newNode = new Node(value);
 71        newNode.next = currentNode.next;
 72        currentNode.next = newNode;
 73    }
 74
 75    //根据值删除结点
 76    delete = (value) =>{
 77        let currentNode = this.head;
 78        let preNode = null;
 79        while(currentNode !== null && currentNode.data !== value){
 80            preNode = currentNode;
 81            currentNode = currentNode.next;
 82        }
 83        if(currentNode == null) return -1; 
 84        preNode.next = currentNode.next;
 85    }
 86
 87     //遍历所有结点
 88    print = () =>{
 89        let currentNode = this.head
 90        //如果结点不为空
 91        while(currentNode !== null){
 92            console.log(currentNode.data)
 93            currentNode = currentNode.next;
 94        }
 95    }
 96}
 97
 98 //测试
 99 const list = new LinkList()
100 list.insert('xiao','head');
101 list.insert('lu','xiao');
102 list.insert('ni','head');
103 list.insert('hellow','head');
104 list.print()
105 console.log('-------------删除元素------------')
106 list.delete('ni')
107 list.delete('xiao')
108 list.print()
109 console.log('-------------按值查找------------')
110 list.findByValue('lu')
111 console.log('-------------按索引查找------------')
112 list.print()

5 测试用例

其实这里的测试用例主要用于判断我们写的程序到底对不对,我们一般都会输入一个自己认为的情况进行测试,这是错误的做法。程序最容易出错的还是边界情况考虑不全面导致的,所以,我们为了能够测试程序的全面性,所以我们要按照以下步骤进行全面性的测试。

5.1 普通测试

普通测试就是输入一个正常的值,比如单链表中插入数据

5.2 特殊测试

特殊输入可以参照上边边界条件中的特殊边界进行测试,比如在头部插入数据,在尾部插入数据等特殊情况的测试。

5.3 输入测试

对于输入测试的内容参考上边边界条件中的输入边界,比如:输入一个空链表测试一下程序能否正常的运行。

6 小结

做一个小结。今天的手写链表主要从五部分下手,从前到后依次为熟悉结构、理清思路、手写代码、测试用例。以后无论手写什么代码,有五步走,对于面试完全没有问题啦。

通过小鹿总结手写链表的方法,不用刻意去背,只要把思路理清楚,边界条件考虑全面,就不用去背,重复的练习。今天的内容就到这里了,如果觉得有帮助,转发文章让更多人看到是对小鹿最大的支持,非常感谢。

发表回复

后才能评论

评论(4)

  • HanPengr 普通 2024年8月10日 上午11:57

    class Node{
    String data;
    Node next;
    public Node(String data){
    this.data = data;
    this.next = null;
    }
    }

    class LinkList {
    private Node head;

    public LinkList(){
        this.head =new Node("head");
    }
    
    // 根据value查找结点
    public Node findByValue(String value){
        Node currentNode = head;
        while (currentNode != null && !currentNode.data.equals(value)){
            currentNode = currentNode.next;
        }
        System.out.println(currentNode);
        return currentNode == null ? null : currentNode;
    }
    
    // 根据index 查找结点
    public Node findByIndex(int index){
        int pos = 0;
        Node currentNode = head;
        while(currentNode != null && pos != index){
            currentNode = currentNode.next;
            pos++;
        }
        //判断是否找到该索引;
        System.out.println(currentNode);
        return currentNode == null ? null : currentNode;
    }
    
    // 插入元素指定元素向后插入
    public void insert(String value,String element){
        //先查找该元素
        Node currentNode = findByValue(element);
        // 如果没有找到
        if (currentNode == null){
            System.out.println("未找到插入位置");
            return;
        }
        Node newNode= new Node(value);
        newNode.next = currentNode.next;
        currentNode.next = newNode;
    }
    
    public void delete(String value){
        Node currentNode = head;
        Node preNode = null;
        while (currentNode != null && !currentNode.data.equals(value)){
            preNode = currentNode;
            currentNode = currentNode.next;
        }
        if (currentNode == null){
            System.out.println("未找到该值!");
            return;
        }
        if(preNode != null){
            preNode.next = currentNode.next;
        }else{
            head = currentNode.next; //如果删除的是头结点:
        }
    }
    
    // 遍历所有结点
    public void print() {
        Node currentNode = head;
        // 如果结点不为空
        while (currentNode != null) {
            System.out.println(currentNode.data);
            currentNode = currentNode.next;
        }
    }
    

    }

    public class LinkedList{
    public static void main(String[] args) {
    LinkList list = new LinkList();
    list.insert(“xiao”, “head”);
    list.insert(“lu”, “xiao”);
    list.insert(“ni”, “head”);
    list.insert(“hellow”, “head”);
    list.print();

        System.out.println("-------------删除元素------------");
        list.delete("ni");
        list.delete("xiao");
        list.print();
    
        System.out.println("-------------按值查找------------");
        list.findByValue("lu");
    
        System.out.println("-------------按索引查找------------");
        list.findByIndex(2);
        list.print();
    }
    

    } Java版本

  • 晚风、 普通 2024年3月2日 下午5:20

    1.链表基本结构:数据域+指针域
    2.以单链表为例:插入和删除的各三种情况,头部/中间/尾部
    链表可以分为:单链表,循环单链表,双链表,循环双链表
    3.特殊情况:属于链表为空,插入链表为空等的情况
    4.插入和删除常规的在中间插入,头部和尾部也是数据特殊情况需要专门测试,保证代码的全面性。
    5.python的实现方式
    Class Node():
    #初始化结点函数
    def init(self,data):
    self.data = data
    self.next = None
    初始化头结点函数
    Class SingleLinkedList():
    def init(self):
    self.head=Node(None)
    def CreateSingleLinkedList(self):
    cNode=self.head
    Eleme=input(“input the data value of this node”)
    while Eleme!=”#”:
    nNode=Node(int(Eleme))
    cNode.next=nNode
    cNode=cNode.next#这一步是什么意思??
    Eleme=input(“请输入当前结点的值”)

  • 711 普通 2024年3月1日 下午8:25

    用javascript写的吗,有没有java版本。。。