您当前的位置: 首页 >  Java

Kevin-Dev

暂无认证

  • 0浏览

    0关注

    544博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【Java -- 数据结构】什么是链表(LinkedList)?

Kevin-Dev 发布时间:2021-03-13 15:55:32 ,浏览量:0

一、前言

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

二、熟悉结构

首先我们要知道链表的结构以及每个节点的结构,这是我们手写链表的第一步,也是学习链表的第一步。我们知道,每个链表时这样表示的: 在这里插入图片描述 那每个节点结构是由数据域和指针域组成,数据域是存放数据的,而指针域存放下一结点的地址。 在这里插入图片描述 我们可以通过数据域访问到我们要的数据,而通过指针域访问到当前结点以后的结点,那么将这些结点串起来,就是一个链表。

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

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

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

function Node(data){
    this.data = data;
    this.next = null;
}

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

class Node{
    constructor(data){
        this.data = data;
        this.next = null;
    }
}
三、理清思路

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

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

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

开始想,插入到单链表的头部算一种。 在这里插入图片描述 然后插入到单链表的中间算一种。 在这里插入图片描述

插入到单链表尾部又算一种。 在这里插入图片描述

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

大家有没有想过如果增加的结点是第一个结点,也就是在空链表中添加新结点的情况呢?所以写代码的时候要把这种特殊情况考虑进去。 然后还有删除的情况,如果为空链表了,就不能删除了。如果删除的是头结点,还需要把头指针向后移动一个,因为当前的头结点被删除无效。

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

四、边界条件

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

1、输入边界

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

2、特殊边界

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

五、代码

1、定义结点 在这里插入图片描述

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

2、增加结点 咱们就以单链表中部添加数据为例子,分解成每个步骤,每个步骤对应代码如下: 2.1 保存临时地址(4结点的地址),需要进行遍历查找到3结点,也就是下列代码的currentNode 结点。 在这里插入图片描述

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

2.2 创建新结点,将新结点(5结点)的指针指向下一结点指针(4结点地址,已经在上一步骤保存下来了) 在这里插入图片描述

let newNode = new Node(value);
newNode.next = pre;

2.3 将3 的结点地址指向新结点(5结点) 在这里插入图片描述

currentNode.next = newNode;

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

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

我们先看删除的全部动画,然后再分步拆分。 在这里插入图片描述 断开3结点的指针(断开3结点相当于让2结点直接指向4结点) 在这里插入图片描述

  let currentNode = this.head;
  // 用来记录 3 结点的前一结点
  let preNode = null;
  // 遍历查找 3 结点
  while(currentNode !== null && currentNode.data !== value){
         // 3 结点的前一结点
         preNode = currentNode;
         // 3 结点
         currentNode = currentNode.next;
}

让结点2的指针指向4结点,完成删除。 在这里插入图片描述

preNode.next = currentNode.next;

所有代码实现:

/定义结点
 class Node{
     constructor(data){
         this.data = data;
         this.next = null;
     }
 }
 
 //定义链表
 class LinkList{
     constructor(){
         //初始化头结点
         this.head = new Node('head');
     }
 
     //根据 value 查找结点
     findByValue = (value) =>{
         let currentNode = this.head;
         while(currentNode !== null && currentNode.data !== value){
             currentNode = currentNode.next;
         }
         //判断该结点是否找到
         console.log(currentNode)
         return currentNode === null ? -1 : currentNode;
     }
 
     //根据 index 查找结点
     findByIndex = (index) =>{
         let pos = 0;
         let currentNode = this.head;
         while(currentNode !== null && pos !== index){
             currentNode = currentNode.next;
             pos++;
         }
         //判断是否找到该索引
         console.log(currentNode)
         return currentNode === null ? -1 : currentNode;
    }
 
     //插入元素(指定元素向后插入)
     insert = (value,element) =>{
         //先查找该元素
         let currentNode = this.findByValue(element);
        //如果没有找到
         if(currentNode == -1){
            console.log("未找到插入位置!")
             return;
         }
         let newNode = new Node(value);
         newNode.next = currentNode.next;
         currentNode.next = newNode;
     }
 
     //根据值删除结点
     delete = (value) =>{
         let currentNode = this.head;
         let preNode = null;
         while(currentNode !== null && currentNode.data !== value){
             preNode = currentNode;
             currentNode = currentNode.next;
         }
         if(currentNode == null) return -1; 
         preNode.next = currentNode.next;
     }
 
      //遍历所有结点
     print = () =>{
         let currentNode = this.head
         //如果结点不为空
         while(currentNode !== null){
             console.log(currentNode.data)
             currentNode = currentNode.next;
         }
     }
 }
 
 //测试
 const list = new LinkList()
list.insert('xiao','head');
list.insert('lu','xiao');
list.insert('ni','head');
list.insert('hellow','head');
list.print()
console.log('-------------删除元素------------')
list.delete('ni')
list.delete('xiao')
list.print()
console.log('-------------按值查找------------')
list.findByValue('lu')
console.log('-------------按索引查找------------')
list.print()
六、测试用例

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

1、普通测试

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

2、特殊测试

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

3、输入测试

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

七、小结

通过总结手写链表的方法,不用刻意去背,只要把思路理清楚,边界条件考虑全面,就不用去背,重复的练习。

关注
打赏
1658837700
查看更多评论
立即登录/注册

微信扫码登录

0.7871s