数据结构的简要介绍:链接列表的工作方式

by Michael Olorunnisola

通过Michael Olorunnisola

数据结构的简要介绍:链接列表的工作方式 (A Gentle Introduction to Data Structures: How Linked Lists Work)

Have you ever built a Rube Goldberg Machine? If not, maybe you’ve built an elaborate line of dominoes?

您是否曾经制造过Rube Goldberg机器? 如果没有,也许您已经建立了详尽的多米诺骨牌系列?

Okay, maybe you weren’t as nerdy of a kid as I was. So be it. For those of you who have had the pleasure to do any of the above, you’ve already grasped the essence of today’s data structure: linked lists!

好吧,也许您不像我那样讨厌小孩子。 就这样吧。 对于那些乐于做上述任何事情的人,您已经掌握了当今数据结构的精髓:链表!

链表如何工作 (How linked lists work)

The simplest form of linked lists — a singly linked list — is a series of nodes where each individual node contains both a value and a pointer to the next node in the list.

链表的最简单形式( 单链表)是一系列节点,其中每个单个节点都包含一个值和一个指向列表中下一个节点的指针。

Additions (Add) grow the list by adding items to the end of the list.

加法( Add )通过将项目添加到列表的末尾来扩大列表。

Removals (Remove) will always remove from a given position in the list.

删除 ( 删除)将始终从列表中的给定位置删除。

Search (Contains) will search the list for a value.

搜索( 包含 )将在列表中搜索值。

Example use cases:

用例示例:

  • Storing values in a hash table to prevent collisions (more on this in a few posts)

    将值存储在哈希表中以防止冲突(有关更多信息,请参见几篇文章)
  • Remaking the amazing race!

    重制惊人的比赛!

Let’s keep this article nice and light by working on a tool that the CBS network can use to plan out their next amazing race TV show.

通过使用CBS网络可以用来计划下一场精彩的竞赛电视节目的工具,让我们保持文章的美好和轻松。

As you go through this, I want you to keep asking yourself: “How are linked lists any different from arrays? How are they similar?”

当您进行此操作时,我希望您不断问自己:“链接列表与数组有何不同? 它们有何相似之处?”

Let’s get started.

让我们开始吧。

First, you need to create the representation of our linked list:

首先,您需要创建链接列表的表示形式:

class LinkedList{  constructor(){    this._head = null;    this._tail = null;    this._length = 0;  }
size(){    return this._length;  }}

To keep track of the starting point and end point of the race, you create the head and tail properties.

要跟踪比赛的起点和终点,请创建head和tail属性。

Then, to make sure you don’t make the race too long or too short for the season, you create a length property and size method. This way, you can always keep track of exactly how long the race is.

然后,为确保您不会在赛季中使比赛变得太长或太短,请创建一个length属性和size方法。 这样,您就可以始终跟踪比赛的时间。

Now that you have a way to store the race list, you should create a way to add to this list. The question is, what are you adding specifically?

现在您已经有了存储比赛列表的方法,现在应该创建一种添加到该列表的方法。 问题是,您要具体添加什么?

Remember, a linked list is a series of nodes where each node has a value and a pointer to the next node in the list. Knowing this, you realize a node is just an object with a value and next property.

请记住,链接列表是一系列节点,其中每个节点都有一个值和一个指向列表中下一个节点的指针。 知道了这一点,您就会意识到节点只是具有值和next属性的对象。

Since you’re going to be creating a new node every time you add to the list, you decide to create a constructor that makes it easier to create a new node for every value that’s added to your list.

由于每次添加到列表时都将创建一个新节点,因此决定创建一个构造函数,该构造函数使为添加到列表中的每个值创建新节点更加容易。

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

Having this constructor available lets you create your add method.

有了此构造函数,您就可以创建自己的add方法。

class Node {  constructor(value) {    this.value = value;    this.next = null;  }}
class LinkedList {   constructor() {    this._head = null;    this._tail = null;    this._length = 0;  }    add(value) {    let node = new Node(value);         //we create our node    if(!this._head && !this._tail){     //If it's the first node      this._head = node;                //1st node is head & tail      this._tail = node;    }else{    this._tail.next = node;             //add node to the back    this._tail = this._tail.next;       //reset tail to last node    }    this._length++;  }    size() {    return this._length;  }}
const AmazingRace = new LinkedList();AmazingRace.add("Colombo, Sri Lanka");AmazingRace.add("Lagos, Nigeria");AmazingRace.add("Surat, India");AmazingRace.add("Suzhou, China");

Now that you’ve added this method, you will be able to add a bunch of locations to your Amazing Race list. This is how it’ll look. Note that I’ve added some extra white space to make it easier to understand.

现在,您已经添加了此方法,您将能够在“惊人的比赛”列表中添加一堆位置。 这就是它的样子。 请注意,我添加了一些额外的空格以使其更易于理解。

{ _head:    { value: 'Colombo, Sri Lanka',     next: { value: 'Lagos, Nigeria',              next: { value: 'Surat, India',                     next: { value: 'Suzhou, China',                             next: null                            }                   }           }    },  _tail: { value: 'Suzhou, China', next: null },  _length: 4 }

Okay, so now that you’ve created this list and a way to add, you realize that you want some help adding locations to this list because you have Decidophobia (yep, it’s a thing).

好的,既然您已经创建了该列表并添加了方法,您将意识到需要一些帮助将位置添加到该列表中,因为您患有Decidophobia(是的,是一件很容易的 )。

You decide to share it with your co-worker, Kent, asking him to add a few more places. The only problem is, when you give it to him, you don’t tell him which places you’ve already added. Unfortunately, you’ve forgotten, too, after suffering amnesia brought on from decision anxiety.

您决定与同事肯特(Kent)共享它,并要求他添加更多位置。 唯一的问题是,当您将其交给他时,您不会告诉他您已经添加了哪些位置。 不幸的是,在因决策焦虑而引起的失忆症之后,您也忘记了。

Of course he could just run console.log(AmazingRace) and read through what the console outputs. But Kent’s a lazy programmer and needs a way to check whether something exists so he can prevent duplicates. With that in mind, you build out a contains method to check for existing values.

当然,他可以只运行console.log(AmazingRace)并阅读控制台输出的内容。 但是肯特是个懒惰的程序员,需要一种方法来检查是否存在某些东西,以便防止重复。 考虑到这一点,您构建了一个contains方法来检查现有值。

class Node {  constructor(value) {    this.value = value;    this.next = null;  }}class LinkedList {   constructor() {    this._head = null;    this._tail = null;    this._length = 0;  }    add(value) {    let node = new Node(value);             if(!this._head && !this._tail){           this._head = node;                      this._tail = this._head;    }else{    this._tail.next = node;                 this._tail = this._tail.next;           }    this._length++;  }    contains(value){    let node = this._head;    while(node){      if(node.value === value){        return true;      }      node = node.next;    }    return false;  }    size() {    return this._length;  }  }
const AmazingRace = new LinkedList();AmazingRace.add("Colombo, Sri Lanka");AmazingRace.add("Lagos, Nigeria");AmazingRace.add("Surat, India");AmazingRace.add("Suzhou, China");
//Kent's check
AmazingRace.contains('Suzhou, China'); //trueAmazingRace.contains('Hanoi, Vietnam'); //falseAmazingRace.add('Hanoi, Vietnam');AmazingRace.contains('Seattle, Washington'); //falseAmazingRace.add('Seattle, Washington');AmazingRace.contains('North Pole'); // falseAmazingRace.add('North Pole');

Awesome, now Kent has a way to check values before adding them, to avoid duplicates.

太棒了,现在Kent有一种在添加值之前检查值的方法,以避免重复。

As an aside, you might be wondering why you didn’t just use the contains method in the add method to prevent duplicate additions? When you’re implementing a linked list — or any data structure, for that matter — you could theoretically add whatever additional functionality you want.

顺便说一句,您可能想知道为什么不只是在add方法中使用contains方法来防止重复添加? 在实现链表或任何数据结构时,从理论上讲,您可以添加所需的任何其他功能。

You can even go in and change native methods on existing structures. Try the below out in a REPL:

您甚至可以进入并在现有结构上更改本机方法。 在REPL中尝试以下操作:

Array.prototype.push = () => { return 'cat';}
let arr = [];arr.push('eggs'); // returns 'cat';

The reason why we don’t do either of these things is because of agreed-upon standards. Essentially, developers have an expectation of how certain methods should work.

我们不做任何一件事情的原因是因为达成了公认的标准 。 本质上,开发人员对某些方法应该如何工作抱有期望。

Since our linked list class isn’t native to JavaScript, we have more freedom in implementing it, but there are still basic expectations of how data structures such as these should function. Linked lists don’t inherently store unique values. But they do have methods like contains that allow us to pre-check and maintain uniqueness in our list.

由于我们的链接列表类不是JavaScript固有的,因此我们在实现它方面有更大的自由度,但是对于诸如此类的数据结构应如何运行仍抱有基本的期望。 链接列表并不是固有地存储唯一值。 但是它们确实具有诸如contains之类的方法,这些方法使我们能够预先检查并维护列表中的唯一性。

Kent gets back to you with his list of destinations, but some of them are questionable. For example, the North Pole might not be the best Amazing Race destination.

肯特(Kent)会列出目的地清单,但其中一些值得怀疑。 例如,北极可能不是最佳的“惊人竞赛”目的地。

So you decide to build out a method to be able to remove a node. It’s important to remember that once you remove the node, you unlink the list, and will have to re-link what came before and after the removed node.

因此,您决定构建一种能够删除节点的方法。 重要的是要记住,一旦删除了节点,便要取消链接列表,并且必须重新链接删除的节点之前和之后的内容。

class Node {  constructor(value) {    this.value = value;    this.next = null;  }}class LinkedList {   constructor() {    this._head = null;    this._tail = null;    this._length = 0;  }    add(value) {    let node = new Node(value);             if(!this._head && !this._tail){           this._head = node;                      this._tail = this._head;    }else{    this._tail.next = node;                 this._tail = this._tail.next;           }    this._length++;  }    remove(value) {    if(this.contains(value)){          // see if our value exists      let current = this._head;           // begin at start of list      let previous = this._head;        while(current){                   // check each node          if(current.value === value){            if(this._head === current){   // if it's the head              this._head = this._head.next;  // reset the head              this._length--;              // update the length              return;                      // break out of the loop            }            if(this._tail === current){   // if it's the tail node              this._tail = previous;       // make sure to reset it            }            previous.next = current.next;  // unlink (see img below)            this._length--;            // update the length            return;                    // break out of           }          previous = current;          // look at the next node          current = current.next;      // ^^        }     }    }      contains(value){    let node = this._head;    while(node){      if(node.value === value){        return true;      }      node = node.next;    }    return false;  }    size() {    return this._length;  }  }
const AmazingRace = new LinkedList();AmazingRace.add("Colombo, Sri Lanka");AmazingRace.add("Lagos, Nigeria");AmazingRace.add("Surat, India");AmazingRace.add("Suzhou, China");AmazingRace.add('Hanoi, Vietnam');AmazingRace.add('Seattle, Washington');AmazingRace.add('North Pole');
//Kent's check
AmazingRace.remove('North Pole');

There’s a lot of code in that remove function up there. Essentially it boils down to the following:

那里的删除功能有很多代码。 从本质上讲,它可以归结为以下几点:

  1. if the value exists in the list…

    如果该值存在于列表中...
  2. iterate over the linked list, keeping track of the previous and current node

    遍历链接列表,跟踪上一个和当前节点
  3. then, if there’s a match →

    然后,如果有比赛→

4A . if it’s the head

4A。 如果是头

  • reset the head to the next node in the list

    将头重置到列表中的下一个节点
  • update the length

    更新长度
  • break out of the loop

    打破循环

4B. if it’s the tail

4B。 如果是尾巴

  • reset the tail to the previous node in the list

    将尾部重置为列表中的上一个节点
  • unlink the node by resetting the pointers as seen below

    通过重置指针来取消链接节点,如下所示

4C. If it’s not a match → continue iterating

4C。 如果不匹配→ 继续迭代

  • make the next node current

    将下一个节点设为当前
  • make the current node previous

    将当前节点设为上一个

One last thing to note: you may have realized that you didn’t actually delete the node. You just removed the references to it. Well, that’s OK because once all references to an object are removed, the garbage collector helps us remove it from memory. You can read up on the garbage collection here.

最后需要注意的一件事:您可能已经意识到实际上并没有删除该节点。 您刚刚删除了对其的引用。 没关系,因为一旦删除了对对象的所有引用,垃圾收集器就会帮助我们将其从内存中删除。 您可以在此处阅读垃圾回收。

With the remove method now implemented, you can run this little piece of code below to make sure contestants don’t freeze to death, or accidentally bother Santa as he’s prepping for this year’s festivities.

使用现已实现的remove方法,您可以在下面运行这段小代码,以确保参赛者不会死机,或者在圣诞老人为今年的庆祝活动做准备时不小心打扰了圣诞老人。

AmazingRace.remove('North Pole');

You’ve done it! You’ve created a simple implementation of a linked list. And you can grow the list by adding items, and shrink it by removing items — all based on the item’s value.

你完成了! 您已经创建了链表的简单实现。 您可以通过添加项目来增加列表,而通过删除项目来缩小列表,所有这些都基于项目的值。

See if you can add you can expand the linked list to allow you to insert values at the beginning, end, or any point in between.

看看是否可以添加,您可以展开链接列表以允许您在开始,结束或之间的任何点插入值。

You have all you need to implement those methods. The names and arguments for these methods should look a little like this:

您拥有实现这些方法所需的全部。 这些方法的名称和参数应该看起来像这样:

addHead(value) {
}
insertAfter(target, value){
}

Feel free to share your implementations in the comments below ?

随时在下面的评论中分享您的实现吗?

队列方法的时间复杂度分析 (A time complexity analysis on the queue methods)

Here’s the code again:

再次是下面的代码:

class LinkedList {   constructor() {    this._head = null;    this._tail = null;    this._length = 0;  }    add(value) {    let node = new Node(value);             if(!this._head && !this._tail){           this._head = node;                      this._tail = this._head;    }else{    this._tail.next = node;                 this._tail = this._tail.next;           }    this._length++;  }    remove(value) {    if(this.contains(value)){                let current = this._head;              let previous = this._head;        while(current){                   if(current.value === value){            if(this._head === current){               this._head = this._head.next;              this._length--;                            return;                                  }            if(this._tail === current){               this._tail = previous;                }            previous.next = current.next;            this._length--;                        return;                              }          previous = current;                    current = current.next;              }     }    }     contains(value){    let node = this._head;    while(node){      if(node.value === value){        return true;      }      node = node.next;    }    return false;  }    size() {    return this._length;  }
// To Be Implemented
addHead(value) {
}
insertAfter(target, value){
}

Add is O(1): Since you always know the last item in the list thanks to tail property, you don’t have to iterate over the list.

加数O(1):由于使用tail属性,您始终知道列表中的最后一项,因此无需迭代列表。

Remove is O(n): In the worst case scenario you’re going to have to iterate over the entire list to find the value to be removed. Great part though is the actual removal of the node is O(1) because you’re just resetting pointers.

RemoveO(n):在最坏的情况下,您将不得不遍历整个列表以找到要删除的值。 尽管很大一部分是该节点的实际删除是O(1),因为您只是在重置指针。

Contains is O(n): You have to iterate over the entire list to check if the value exists in your list.

包含O(n):您必须遍历整个列表以检查该值是否存在于列表中。

addHead is O(1): Similar to our add method above, we always know the position of the head, so no iteration necessary.

addHeadO(1):与上面的add方法类似,我们始终知道头部的位置,因此不需要迭代。

insertAfter is O(n): Similar to our Remove method above, you’ll have to iterate over the entire list to find the target node that your value should be inserted after. Likewise, the actual insertion is O(1) because you’re just resetting pointers.

insertAfterO(n) :类似于上面的Remove方法,您必须遍历整个列表以找到应该在其后插入值的目标节点。 同样,实际插入是O(1),因为您只是在重置指针。

链表与阵列? (Linked List vs Array?)

Why would you use a linked list instead of an arrays? Arrays technically allow you to do all of the things linked lists do, such as additions, insertions, and removals. Also, all these methods are already readily available to us in JavaScript.

为什么要使用链表而不是数组? 从技术上讲,数组使您可以执行链表的所有操作,例如添加,插入和删除。 同样,所有这些方法都已经可以在JavaScript中使用。

Well, the biggest difference comes in the insertions and removals. Since arrays are indexed, when you perform an insertion or removal in the middle of the array, you have to reset the position of all following values to their new indices.

好吧,最大的不同在于插入和删除。 由于数组已建立索引,因此在数组中间执行插入或删除操作时,必须将所有后续值的位置重置为其新索引。

Imagine inserting into the start or middle of an array 100,000 values long! Insertions and removals like this are extremely expensive. Because of this, linked lists are often preferred for large data sets that are often shifted around.

想象一下,将一个100,000个值的数组插入数组的开头或中间! 这样的插入和移除非常昂贵。 因此,对于经常移动的大型数据集,链表通常是首选。

On the other hand, arrays are great when it comes to finding items (random access) since they are indexed. If you know the position of an item, you can access it in O(1) time via array[position].

另一方面,在查找项目(随机访问)时,数组非常有用,因为它们已被索引。 如果知道项目的位置,则可以在O(1)时间内通过array [position]访问它。

Linked lists always require you to iterate over the linked lists sequentially. Given this, arrays are usually preferred for either smaller data sets, or data sets that aren’t shifted around as often.

链接列表始终要求您按顺序遍历链接列表。 鉴于此,通常对于较小的数据集或不经常移动的数据集通常首选数组。

是时候快速回顾一下 (Time for a quick recap)

Linked Lists:

链接列表:

  1. have a tail and head property to track the ends of the list

    具有tail和head属性以跟踪列表的结尾
  2. have an add, addHead, insertAfter, and remove method to manage the contents of your list

    有一个add,addHead,insertAfter和remove方法来管理列表的内容
  3. have a length property to track how long your linked list is

    具有length属性来跟踪链表的长度

进一步阅读 (Further Reading)

There are also the doubly-linked list and circular-linked list data structures. You can read about them on Wikipedia.

还有双向链表和循环链表数据结构。 您可以在Wikipedia上阅读有关它们的信息

Also, here’s a solid, quick overview by Vivek Kumar.

另外,这是Vivek Kumar的扎实,快速的概述

Finally, Ian Elliot wrote a walk-through that helps you implementing all of the methods. But see if you can implement addHead() and insertAfter() for your linked list before peeking at this ?

最后,伊恩·艾略特写了一个步行通过 ,可以帮助你实现所有的方法。 但是,先看看您是否可以为链接列表实现addHead()insertAfter()

翻译自: https://www.freecodecamp.org/news/a-gentle-introduction-to-data-structures-how-linked-lists-work-5adc793897dd/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值