算法基础篇-链表

340 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

在上一篇文章中,我们说到了栈与队列,这章中,我们来看看另一个基础的知识,链表

链表

链表是存储的有序元素的集合,但是不同于数组连续存储方式,链表在存储中并不是采用连续放置的方式,而是每一个元素由节点和指向下一个元素的引用(其他语言中的指针)。所以我们可以得知链表的具体的形式应该是

image.png

那么我们将上图进行拆分,我们可以很明确的得到一个链表应该有的数据结构,如下图:

image.png

从上图可以得知,一个链表我们可以将其拆分List类,和node类。所以我们可以先实现这两个类

Node类
/**
 * @export
 * @class LinkNode
 */
export default class LinkNode {
    [x: string]: undefined;
    public node:any;
    public next: undefined;
    constructor(node:any) {
        this.node = node;
        this.next = undefined;
    }
}
List类
 *
 * @export
 * @class LinkList
 */
import LinkNode from './LinkNode'
export default class LinkList {
    public count: number;
    public head: any | undefined;
    constructor() {
        this.count = 0;
        this.head = undefined;
    }
}

那么一个链表应该需要哪些方法呢?从个人理解中,同队列和栈一样需要以下方法

方法名描述
push(item)添加一个元素到链表中
insert(item,index)添加一个元素到链表指定位置中
getItemAt(index)获取链表指定位置元素
remove()移除最后一项
clear()清除链表
size()返回链表大小
removeAt(index)移除指定位置元素并返回
indexOf(item)获取指定元素所在的位置
下面我们来一一分析上面方法的具体实现
Push

向链尾添加元素。那么有两种情况,一种是最初始状态下添加,根据我们前面的list类,我们可以看出,当前数据格式为,也就是一个head指向的是undefined。那么我们添加一个数据后,我们的数据格式应该为什么样呢,第二种情况为在已有的数据格式的基础下添加,那么我们可以思考下怎么实现呢?

我们来分析下,也就是当前最后一个变成了倒数第二个,push进来的node为最后一个,所以只需要将当前最后一个的next指向当前传入的即可。如图

image.png 因此Push方法就此实现

 /**
     *尾部插入元素
     *
     * @param {*} item
     * @memberof LinkList
     */
    public push(item: any) {
        let node = new LinkNode(item);
        // 将节点的head设置成node
        if (!this.head) {
            this.head = node;
        } else {
            // 如果不是空,则需要找到链表最后一个node,然后将最后一个节点的next设置成当前传入的node
            let currentNode: any = this.getItemAt(this.count - 1);
            currentNode.next = node;
        }
        this.count++;
    }
removeAt(index)

从指定位置移除元素。我们可以先来分析下,移除有三种情况 第一种,只移除头部,只需要将head指向第二个节点即可,同时接触第一个节点next指向第二个节点,所以也就有了这样的形式

image.png

第二种移除尾部,类比移除头部,只需要将倒数第二个节点的next指向为undefined即可,如图

image.png 第三种就是删除中间位置,同理只需要将index所处的节点前一个节点的next指向index所处节点的next节点即可

image.png 具体实现如下

 /**
     * 移除指定位置的节点
     * @param {number} index
     * @returns {LinkNode}
     * @memberof LinkList
     */
    public removeAt(index: number): LinkNode | undefined {
        if (index >= 0 && index <= this.count) {
            let cuttentNode: LinkNode = this.getItemAt(index);
            if (index === this.count - 1) {
                let pretNode: LinkNode = this.getItemAt(index - 1);
                pretNode.next = undefined;
            } else if (index === 0) {
                let headNode: LinkNode = this.getItemAt(0);
                headNode.head = undefined;
            } else {
                let preNode: LinkNode = this.getItemAt(index - 1);
                let currentNode: LinkNode = this.getItemAt(index);
                preNode.next = currentNode.next;
            }
            this.count--;
            return cuttentNode;
        }
        return undefined;
    }
insert(item,index)

在指定位置插入元素。我们来分析分析,我们选择插入元素,那么同移除有三个位置,头部,尾部以及中间

首先我们看下从头部移除,如果头部插入元素的话,那么也就是当前head指向的为插入的元素,插入元素的next指向head以前指向的元素。所以如下图:

image.png

第二种情况,从尾部插入,也就是将当前尾部元素的next指向我们插入的元素,我们插入的元素的next指向原有元素next指向的位置

image.png 第三种情况插入中间位置,那么也就是说假设我们将数据插入第二个位置,我们只需要将第一个位置节点的next指向插入的元素,插入的元素的next指向当前元素即可。如图

image.png

这个时候我们再回顾下从尾部插入,我们是否也可以跟中间节点一样,将最后一个节点的前一个节点的next赋值给当前节点的next,前一个节点的next指向插入的节点,貌似也行,那么代码实现为。

/**
     * 任意位置插入元素
     * @param {*} item
     * @param {number} index
     * @returns {boolean}
     * @memberof LinkList
     */
    public insert(item: any, index: number): boolean {
        if (index >= 0 && index <= this.count) {
            let node = new LinkNode(item);
            if (index === 0) {
                let currentNode = this.head;
                node.next = currentNode;
                this.head = node;
            } else {
                let preNode = this.getItemAt(index - 1);
                node.next = preNode.next;
                preNode.next = node;
            }
            this.count++;
            return true;
        }
        return false;
    }

至此一个普通链表的最重要的三个方法讲完了,下面附上一个链表的具体的实现(后面附上git仓库地址).下一章我们进入另一个链表-双向链表