第三节:链表详解和面试题剖析

一. 链表详解

1. 对比数组

 数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容。 (一般情况下是申请一个更大的数组,比如2倍。 然后将原数组中的元素复制过去)

 而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移

2. 链表的优势

3. 什么是链表?

 链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推。

 

二. 手写链表

1. 链表结构封装

   封装一个Node类,用于封装每一个节点上的信息(包括值和指向下一个节点的引用),它是一个泛型类。

   封装一个LinkedList类,用于表示我们的链表结构。   链表中我们保存两个属性,一个是链表的长度,一个是链表中第一个节点head

class Node<T> {
	value: T;
	next: Node<T> | null = null;
	constructor(val: T) {
		this.value = val;
	}
}

class LinkedList<T> {
	head: Node<T> | null = null;
	private size: number = 0; //内部使用
	//对外开放
	get length() {
		return this.size;
	}
}

2.  常用方法和重点区分

(1). 常用方法

         append(val):向链表尾部添加一个新的项
         traverse(): 遍历链表
         insert(val, position):向链表的特定位置插入一个新的项。
         removeAt(position):从链表的特定位置移除一项。
         get(position) :获取对应位置的元素
         indexOf(val):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
         update(val, position) :修改某个位置的元素
         remove(val):从链表中移除一项。
         isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false。
         size():返回链表包含的元素个数。与数组的length属性类似

(2). 重点区分 【重要】

      A=B       表示A和B都是相同的,可以理解成A和B在内存中指向同一个区域

      A.next=B  表示A是B的前一个节点

      A=A.next  表示A和A.next在内存中指向同一个区域,即将A向后移动了1位

3. append方法

分析:链表本身为空,新添加的数据时唯一的节点。 链表不为空,需要向其他节点后面追加节点

注:这里的while循环中的条件是 current.next,而不是current;因为current.next=null, 表示此时current是最后一个节点。

  /**
	 * 1. 向尾部插入元素
	 * @param val 插入的元素
	 * 
	 * while循环完成后,current指向了最后一个节点,那么为什么将current.next指向newNode,就代表最后一个节点执行newNode呢?
	   ypf自己理解:
		假设当前的节点为   head → A →B →C →	D			
		理解1:声明一个current节点,把head赋值给current,那么head和current都是指向A
		  然后将current.next赋值给current(相当于B赋值给了current),
		  那么此时current指向了B,依次类推,直到:将D赋值给了current,此时current.next为空了,while循环结束		
		  那么此时将 current.next 指向newNode,也就相当于D.next指向了newNode,即在最后位置添加了节点。			
		理解2:比如将 head赋值给current,即current=head,那么他们在内存中指向同一个区域。
		while循环中的最后一步,将D赋值给了current,二者指向内存的同一个区域,改变任何一个next指向,另外一个也跟着改了。
			
	 */
	append(val: T): void {
		let newNode = new Node(val);
		// 链表本身为空
		if (!this.head) {
			this.head = newNode; //将newNode赋值给head(用的不是next属性指向)
		}
		//链表不为空
		else {
			let current = this.head; //声明一个current节点,并将头节点赋值给它
			/* 
				不知道数量的情况下,使用while遍历, current.next存在,则表示还有下一个节点,则将当前节点指向下一个节点(下一个节点
				赋值给current,实现current的移位)
			*/
			while (current.next) {
				current = current.next;
			}
			//遍历完成,表示current.next为null了,此时current是最后一个节点,那么将其next指向新节点即可
			current.next = newNode;
		}
		this.size++; //最后数量+1
	}

4. traverse方法

   在while循环中,我们遍历链表并打印当前结点的数据。 在每次迭代中,我们将当前结点设置为其下一个结点,直到遍历完整个链表。

   /**
	 * 2. 遍历链表
	 * 以string拼接的形式输出
	 */
	traverse(): string {
		let array: T[] = []; //用数组来存储
		let current = this.head;
		while (current) {
			array.push(current.value);
			current = current.next;
		}
		return array.join("->");
	}

5. insert方法

分析:

 (1). 添加到第一个位置

 (2). 添加到其它位置,包括最后

    /**
	 * 3. 任意位置插入
	 * @param val 插入的元素
	 * @param position 插入位置,索引从0开始(0时插入在最前面,1时插入在1节点和2节点之间,2时插入在2节点和3节点之间)
	 *                 通常称呼:节点1、节点2、节点3, 没有节点0;
	 *
	 * 重点区分:A=B 表示A和B都是相同的,可以理解成A和B在内存中指向同一个区域
	 *          A.next=B  表示A是B的前一个节点
	 *
	 */
	insert(val: T, position: number): boolean {
		// 非法位置,越界了
		if (position < 0) return false;
		let newNode = new Node(val); // 创建新节点
		// 向头位置插入
		if (position == 0) {
			newNode.next = this.head; //这里head节点就是第一个节点
			this.head = newNode;
		}
		// 向中间位置或者最后位置插入
		else {
			let current = this.head; //当前节点
			let previous: Node<T> | null; //前一个节点
			let index = 0;
			while (current && index < position) {
				previous = current;
				current = current.next;
				index++;
			}
			newNode.next = current;
			previous!.next = newNode;
		}
		this.size++;
		return true;
	}

6. removeAt方法

分析:

 (1). 移除第一项:移除第一项时,直接让head指向第二项信息就可以啦。 那么第一项信息没有引用指向,就在链表中不再有效,后面会被回收掉。

   (2). 移除其它项:首先,我们需要通过while循环,找到正确的位置。 找到正确位置后,就可以直接将上一项的next指向current项的next,这样中间的项就没有引用指向它,也就不再存
在于链表后,会面会被回收掉。 

/**
	 * 4. 删除指定位置节点
	 * @param position  删除的位置,索引从0开始(0时删除在第1个节点,1时表示删除第2个节点,索引的最大值比length小1)
	 * @returns 返回删除的节点值 或者  null
	 * 几个注意的点:
	 *  (1). 找到正确位置后,就可以直接将上一项的next指向current项的next,这样中间的项就没有引用指向它,也就不再存在于链表后,会面会被回收掉
	 *  (2). while遍历完后,只操作了previous节点,此时current还指向一个节点,但它是局部遍历,完成后就消失了
	 */
	removeAt(position: number): T | null {
		//1.越界处理
		if (position < 0 || position >= this.length) return null;
		//2.正常的索引
		let current = this.head;
		let previous: Node<T> | null;
		//2.1 删除第1个节点
		if (position === 0) {
			//这里要考虑是否总共1个节点
			// this.head = this.length === 1 ? null : current!.next;
			//等价于
			this.head = current?.next ?? null;
		}
		//2.2 删除中间节点(即第二个 或 以后的节点)
		else {
			let index = 0;
			//遍历完成后,current即position对应的节点,previous即current的前一个节点
			while (current && index < position) {
				previous = current;
				current = current.next;
				index++;
			}
			//将previous跨过1个节点,指向下一个节点(此时:current还指向一个节点,但是current是个局部遍历,调用完就消失了)
			// previous!.next = current == null ? null : current.next;
			// 等价于
			previous!.next = current?.next ?? null;
		}
		this.size--;
		return current?.value ?? null;
	}

7. get方法

   获取对应位置的元素

	/**
	 * 5. 获取指定位置的元素
	 * @param position 指定位置,索引从0开始
	 * @returns  返回指定节点的value 或者  节点为空返回null
	 */
	get(position: number): T | null {
		//1 越界问题
		if (position < 0 || position >= this.size) return null;
		// 2.查找元素
		let current = this.head;
		let index = 0;
		while (current && index < position) {
			current = current.next;
			index++;
		}
		return current?.value ?? null;
	}

8. indexOf方法

   根据元素获取它在链表中的位置

   /**
	 * 6. 根据内容值返回索引
	 * @param val 内容值
	 * @returns 返回的所以,不存在则返回-1
	 */
	indexOf(val: T): number {
		let current = this.head;
		let index = 0;
		while (current) {
			if (current.value === val) return index;
			current = current.next; //向后移位
			index++;
		}
		return -1; //返回-1,表示不存在
	}

9. update方法

  修改某个位置的元素

/**
	 * 7. 更新指定位置的元素
	 * @param val 更新的元素
	 * @param position 更新的位置
	 * @returns 成功返回true,失败返回false
	 */
	update(val: T, position: number): boolean {
		// 1. 越界
		if (position < 0 || position >= this.size) return false;
		//2. 找到指定位置的元素进行更新
		let current = this.head;
		let index = 0;
		while (current && index < position) {
			current = current.next; //移位
			index++;
		}
		//进行更新
		current!.value = val;
		return true;
	}

10. isEmpty方法

   判断链表是否为空

   /**
	 * 8. 判断链表是否为空
	 * @returns true or false
	 */
	isEmpty(): boolean {
		return this.size === 0;
	}

11. remove方法

    根据元素值删除(只删除队列中第一次出现的)
	/**
	 * 9. 根据元素值删除(只删除队列中第一次出现的)
	 * @param element 待删除的值
	 * @returns 删除的元素
	 */
	remove(val: T): T | null {
		// 获取索引
		let index = this.indexOf(val);

		// 根据索引删除
		let result = this.removeAt(index);

		return result;
	}

 

三. 链表重构

1.  封装获取指定位置的节点

    将遍历节点,获取指定位置节点的操作重构,封装为getNode方法, 修改的方法有:get、update、insert、removeAt  。
	/**
	 * 0.获取指定位置的节点
	 * @param position 指定位置
	 * @returns 返回节点,可能为null
	 */
	private getNode(position: number): null | Node<T> {
		let current = this.head;
		let index = 0;
		while (current && index < position) {
			current = current.next;
			index++;
		}
		return current;
	}

2. 抽离接口

  抽离接口

/* 
  链表接口类
*/

interface ILinkedList<T> {
	append(val: T): void;
	traverse(): string;
	insert(val: T, position: number): boolean;
	removeAt(position: number): T | null;
	get(position: number): T | null;
	indexOf(val: T): number;
	update(val: T, position: number): boolean;
	isEmpty(): boolean;
	remove(val: T): T | null;
}

export default ILinkedList;

分享完整代码: 

/* 
   1. 封装获取指定位置的节点。
      将遍历节点,获取指定位置节点的操作重构,封装为getNode方法
      修改的方法有:get、update、insert、removeAt
   2. 抽离接口    
*/

import ILinkedList from "./interface/ILinkedList";

/**
 * 节点类
 */
class Node<T> {
    value: T;
    next: Node<T> | null = null;
    constructor(val: T) {
        this.value = val;
    }
}

/**
 * 单链表类
 */
class LinkedList<T> implements ILinkedList<T> {
    head: Node<T> | null = null; //就是头节点!!!(就是第一个节点)
    private size: number = 0; //内部使用
    //对外开放
    get length() {
        return this.size;
    }

    /**
     * 0.获取指定位置的节点
     * @param position 指定位置
     * @returns 返回节点,可能为null
     */
    private getNode(position: number): null | Node<T> {
        let current = this.head;
        let index = 0;
        while (current && index < position) {
            current = current.next;
            index++;
        }
        return current;
    }

    /**
     * 1. 向尾部插入元素
     * @param val 插入的元素
     * 
     * while循环完成后,current指向了最后一个节点,那么为什么将current指向newNode,就代表最后一个节点执行newNode呢?
       ypf自己理解:
        假设当前的节点为   head → A →B →C →    D            
        理解1:声明一个current节点,把head赋值给current,那么head和current都是指向A
          然后将current.next赋值给current(相当于B赋值给了Current),
          那么此时current指向了B,依次类推,直到:将D赋值给了current,此时current.next为空了,while循环结束        
          那么此时将 current.next 指向newNode,也就相当于D.next指向了newNode,即在最后位置添加了节点。            
        理解2:比如将 head赋值给current,即current=head,那么他们在内存中指向同一个区域。
        while循环中的最后一步,将D赋值给了current,二者指向内存的同一个区域,改变任何一个next指向,另外一个也跟着改了。
            
     */
    append(val: T): void {
        let newNode = new Node(val);
        // 链表本身为空
        if (!this.head) {
            this.head = newNode; //将newNode赋值给head(用的不是next属性指向)
        }
        //链表不为空
        else {
            let current = this.head; //声明一个current节点,并将头节点赋值给它
            /* 
                不知道数量的情况下,使用while遍历, current.next存在,则表示还有下一个节点,则将当前节点指向下一个节点(下一个节点
                赋值给current,实现current的移位)
            */
            while (current.next) {
                current = current.next;
            }
            //遍历完成,表示current.next为null了,此时current是最后一个节点,那么将其next指向新节点即可
            current.next = newNode;
        }
        this.size++; //最后数量+1
    }
    /**
     * 2. 遍历链表
     * 以string拼接的形式输出
     */
    traverse(): string {
        let array: T[] = []; //用数组来存储
        let current = this.head;
        while (current) {
            array.push(current.value);
            current = current.next;
        }
        return array.join("->");
    }
    /**
     * 3. 任意位置插入
     * @param val 插入的元素
     * @param position 插入位置,索引从0开始(0时插入在最前面,1时插入在1节点和2节点之间,2时插入在2节点和3节点之间)
     *
     * 重点区分:A=B 表示A和B都是相同的,可以理解成A和B在内存中指向同一个区域
     *          A.next=B  表示A是B的前一个节点
     *
     */
    insert(val: T, position: number): boolean {
        // 非法位置,越界了
        if (position < 0) return false;
        let newNode = new Node(val); // 创建新节点
        // 向头位置插入
        if (position == 0) {
            newNode.next = this.head; //这里head节点就是第一个节点
            this.head = newNode;
        }
        // 向中间位置或者最后位置插入
        else {
            // let current = this.head; //当前节点
            // let previous: Node<T> | null; //前一个节点
            // let index = 0;
            // while (current && index < position) {
            //     previous = current;
            //     current = current.next;
            //     index++;
            // }
            // newNode.next = current;
            // previous!.next = newNode;

            let previous = this.getNode(position - 1);
            newNode.next = previous!.next; //previous.next 相当于current
            previous!.next = newNode;
        }
        this.size++;
        return true;
    }
    /**
     * 4. 删除指定位置节点
     * @param position  删除的位置,索引从0开始(0时删除在第1个节点,1时表示删除第2个节点,索引的最大值比length小1)
     * @returns 返回删除的节点值 或者  null
     * 几个注意的点:
     *  (1). 找到正确位置后,就可以直接将上一项的next指向current项的next,这样中间的项就没有引用指向它,也就不再存在于链表后,会面会被回收掉
     *  (2). while遍历完后,只操作了previous节点,此时current还指向一个节点,但它是局部遍历,完成后就消失了
     */
    removeAt(position: number): T | null {
        //1.越界处理
        if (position < 0 || position >= this.length) return null;
        //2.正常的索引
        let current = this.head;
        //2.1 删除第1个节点
        if (position === 0) {
            //这里要考虑是否总共1个节点
            // this.head = this.length === 1 ? null : current!.next;
            //等价于
            this.head = current?.next ?? null;
        }
        //2.2 删除中间节点(即第二个 或 以后的节点)
        else {
            // {
            // let previous: Node<T> | null;
            //     let index = 0;
            //     //遍历完成后,current即position对应的节点,previous即current的前一个节点
            //     while (current && index < position) {
            //         previous = current;
            //         current = current.next;
            //         index++;
            //     }
            //     //将previous跨过1个节点,指向下一个节点(此时:current还指向一个节点,但是current是个局部遍历,调用完就消失了)
            //     // previous!.next = current == null ? null : current.next;
            //     // 等价于
            //     previous!.next = current?.next ?? null;
            // }

            let previous = this.getNode(position - 1);
            previous!.next = previous?.next?.next ?? null;
        }
        this.size--;
        return current?.value ?? null;
    }
    /**
     * 5. 获取指定位置的元素
     * @param position 指定位置,索引从0开始
     * @returns  返回指定节点的value 或者  节点为空返回null
     */
    get(position: number): T | null {
        //1 越界问题
        if (position < 0 || position >= this.size) return null;

        // 2. 获取指定节点
        let current = this.getNode(position);

        //3. 返回结果(?表示左边为null时,不报错,返回undefined)
        return current?.value ?? null;
    }
    /**
     * 6. 根据内容值返回索引
     * @param val 内容值
     * @returns 返回的所以,不存在则返回-1
     */
    indexOf(val: T): number {
        let current = this.head;
        let index = 0;
        while (current) {
            if (current.value === val) return index;
            current = current.next; //向后移位
            index++;
        }
        return -1; //返回-1,表示不存在
    }
    /**
     * 7. 更新指定位置的元素
     * @param val 更新的元素
     * @param position 更新的位置
     * @returns 成功返回true,失败返回false
     */
    update(val: T, position: number): boolean {
        // 1. 越界
        if (position < 0 || position >= this.size) return false;
        //2. 找到指定位置的元素进行更新
        let current = this.getNode(position);
        //3.进行更新
        current!.value = val;
        return true;
    }
    /**
     * 8. 判断链表是否为空
     * @returns true or false
     */
    isEmpty(): boolean {
        return this.size === 0;
    }
    /**
     * 9. 根据元素值删除(只删除队列中第一次出现的)
     * @param element 待删除的值
     * @returns 删除的元素
     */
    remove(val: T): T | null {
        // 获取索引
        let index = this.indexOf(val);

        // 根据索引删除
        let result = this.removeAt(index);

        return result;
    }
}

export default LinkedList;
View Code

 

四. 面试题剖析

1. 设计链表

  你可以选择使用单链表或者双链表,设计并实现自己的链表。

  leetcode:https://leetcode.cn/problems/design-linked-list/description/

(1). 要求:

  单链表中的节点应该具备两个属性:val 和 next 。val 是当前节点的值,next 是指向下一个节点的指针/引用。

(2). 实现 MyLinkedList 类:

    MyLinkedList() 初始化 MyLinkedList 对象。

    int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1 。

    void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。

    void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。

    void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。

    void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

代码分享:

查看代码
 class Node {
	val: number;
	next: Node | null = null;
	constructor(val: number) {
		this.val = val;
	}
}

class MyLinkedList {
	head: Node | null = null;
	size: number = 0;

	get(position: number): number {
		if (position < 0 || position >= this.size) return -1;
		let current = this.head;
		let index = 0;
		while (current && index < position) {
			current = current.next;
			index++;
		}
		return current!.val;
	}

	addAtIndex(position: number, val: number): void {
		if (position < 0 || position > this.size) return;
		let current = this.head;
		let newNode = new Node(val);
		if (position == 0) {
			newNode.next = current;
			this.head = newNode;
		} else {
			let previous: Node | null = null;
			let index = 0;
			while (current && index < position) {
				previous = current;
				current = current.next;
				index++;
			}
			newNode.next = current;
			previous!.next = newNode;
		}
		this.size++;
	}

	addAtHead(val: number): void {
		this.addAtIndex(0, val);
	}

	addAtTail(val: number): void {
		this.addAtIndex(this.size, val);
	}

	deleteAtIndex(position: number): void {
		if (position < 0 || position >= this.size) return;
		let current = this.head;
		if (position == 0) {
			this.head = current?.next || null;
		} else {
			let previous: Node;
			let index = 0;
			while (current && index < position) {
				previous = current;
				current = current.next;
				index++;
			}
			previous!.next = current?.next ?? null;
		}
		this.size--;
	}
	traverse(): string {
		let array: number[] = [];
		let current = this.head;
		while (current) {
			array.push(current.val);
			current = current.next;
		}
		return array.join("=>");
	}
}

// 开始测试
let link = new MyLinkedList();
link.addAtHead(1);
link.addAtTail(3);
console.log(link.traverse());
link.addAtIndex(1, 2);
console.log(link.traverse());

console.log(link.get(1)); // 返回 2
link.deleteAtIndex(1); // 现在,链表变为 1->3
console.log(link.get(1)); // 返回 3
console.log(link.traverse());

export default MyLinkedList;

 

2. 删除node节点

 leetcode:https://leetcode.cn/problems/delete-node-in-a-linked-list/description/

  要求:

     1. 有一个单链表的 head,我们想删除它其中的一个节点 node

     (1).给你一个需要删除的节点 node

     (2).你将 无法访问 第一个节点 head。

     2. 链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

     3. 删除给定的节点。注意,删除节点并不是指从内存中删除它。这里的意思是:

        • 给定节点的值不应该存在于链表中。

        • 链表中的节点数应该减少 1。

        • node 前面的所有值顺序相同。

        • node 后面的所有值顺序相同。

   核心思路:

      假设被删除的节点为current,那么让current的value 等于 current.next的value;被删除的节点指向 current.next.next

代码分享:

class ListNode {
	val: number;
	next: ListNode | null;
	constructor(val?: number, next?: ListNode | null) {
		this.val = val === undefined ? 0 : val;
		this.next = next === undefined ? null : next;
	}
}

/**
 * 删除节点
 * @param node 需要被删除的节点
 */
function deleteNode(node: ListNode | null): void {
	node!.val = node!.next!.val;
	node!.next = node!.next!.next;
}

export {};

 

3. 反转链表--栈

  leetcode:https://leetcode.cn/problems/reverse-linked-list/

  要求:

    给你单链表的头节点 head ,请你反转链表,并返回反转后的链表的头节点。

   详见:https://www.cnblogs.com/yaopengfei/p/18067536

 

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-11-16 08:30  Yaopengfei  阅读(23)  评论(1编辑  收藏  举报