快速掌握单链表和双链表(含面试题)

1. 为什么有链表

1.1 顺序表优缺点

【顺序表的优点】

  • 给定下标进行查找时,时间复杂度为O(1)

【顺序表的缺点】

  • 插入/删除数据时,必须移动数据,最坏情况是在0下标插入/删除数据,时间复杂度为O(N)
  • 扩容有可能浪费内存

1.2 链表

【链表的优点】

  1. 插入/删除数据时,不必移动数据,时间复杂度为O(1)
  2. 扩容不会浪费内存

【链表的缺点】查找数据,时间复杂度为O(N)

【总结】顺序表只适合给定下标查找,链表则适合插入/删除,两者正好互补

2. 什么是链表

【概念】
在这里插入图片描述

  1. 链表底层是一个个串起来的节点,其物理(内存)上不连续,逻辑上连续;
  2. 链表是通过节点进行的连接

【与顺序表的区别】

  1. 比数组多了一个变量来计数;
  2. 原因是,按照遍历思路计算数组大小(遇到0终止),若数组中有一个值就是0呢?所以用变量计数,插入一个数据,负责计数的变量就++;

【链表类型】

  1. 链表类型一共有9种,但只重点掌握两种即可
  2. 分别是“单向不带头节点的非循环链表(SingleList)”和“双向不带头节点的非循环链表(LinkList)”

【小经验】

  1. 想要遍历整个链表,while (cur != null)
  2. 想要使cur停留在最后一个,while(cur.next != null)

3. 实现单向链表CRUD

3.1 题目

【复制到IList接口内】

//头插法
void addFirst(int data);
//尾插法
void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
boolean contains(int key);
//删除第一次出现关键字为key的节点
void remove(int key);
//删除所有值为key的节点
void removeAllKey(int key);
//得到单链表的长度
int size();
//清空链表
void clear();
//打印链表
void display();

3.2 实现SingleList

【前期准备】

  1. 准备链表的两个域:数据域val和next域,提供val的构造方法
  2. 准备链表的头节点:head
  3. 创建链表(用头插/尾插创建链表)

【代码实现】

public class MySingleList implements IList {  
    //节点内部类定义val和next
    static class ListNode{  
        public int val;  
        public ListNode next;  
  
        public ListNode(int val) {  
            this.val = val;  
        }  
    }  
    //创建头节点
    private ListNode head;  
    //创建链表
    private void createList () {  
        ListNode node1 = new ListNode(12);  
        ListNode node2 = new ListNode(23);  
        ListNode node3 = new ListNode(34);  
        ListNode node4 = new ListNode(45);  
  
        node1.next = node2;  
        node2.next = node3;  
        node3.next = node4;  
  
        this.head = node1;  
    }
}

【注】

  1. 想要遍历整个链表,while (cur != null)
  2. 想要使cur停留在最后一个,while(cur.next != null)
0. 打印链表

【代码逻辑】

  1. 找思路:链表是通过每个节点的next域来遍历的,当节点为null时,遍历完成
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:无

【代码实现】

public void display() {  
    //cur指向头节点,通过next遍历链表
    ListNode cur = this.head;  
    //通过next遍历链表
    while (cur !=null) {  
        System.out.print(cur.val+" ");  
        cur = cur.next;  
    }  
    System.out.println();  
}
1. 增(3道)

【需考虑】

  1. index合法性
  2. 判断是头插,尾插还是中间插,不同情况分开讨论

1.1 头插法
【画图】
在这里插入图片描述

【代码逻辑】

  1. 找思路:创建节点;节点的next域存head;head指向节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:无

【代码实现】

public void addFirst(int data) {  
    //创建一个节点,传data  
    ListNode node = new ListNode(data);  
    //该节点的next域存原来的头节点  
    node.next = this.head;  
    //原来的头节点替换成新插入的节点  
    this.head = node;  
}

1.2 尾插法
【画图】
在这里插入图片描述

【代码逻辑】

  1. 找思路:创建节点;通过next域遍历链表到最后一个节点;更改最后一个节点的next域为插入节点的地址
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:无

【代码实现】

public void addLast(int data) {  
    //创建一个节点,传data  
    ListNode node = new ListNode(data);  
    ListNode cur = this.head;  
    //通过next域遍历链表到最后一个节点  
    while (cur.next !=null) {  
        cur = cur.next;  
    }  
    //更改最后一个节点的next域为插入节点的地址  
    cur.next = node;  
}

1.3 任意位置插入,第一个数据节点为0号下标

【代码逻辑】

  1. 找思路:创建节点;获取到index-1下标要走几步;更改index-1的next和节点的next
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:index合法性;分成头插,尾插和中间插;头插和尾插要return

【代码实现】

//获取index-1下标指向的节点
private ListNode getIndex(int index) {  
    ListNode cur = this.head;  
    while ( index-1 >0 ) {  
        cur = cur.next;  
        index--;  
    }  
    return cur;  
}

public void addIndex(int index, int data) {  
    //检查index合法性  
    if (index < 0 || index > size()) {  
        throw new RuntimeException();  
    }  
    //实例化节点  
    ListNode node = new ListNode(data);  
    //获取到index-1下标要走几步  
    ListNode cur = getIndex(index);  
    //头插法  
    if (index == 0) {  
        addFirst(data);  
        return;    }  
    //尾插法  
    if (index == size()) {  
        addLast(data);  
        return;    }  
    //中间插  
    node.next = cur.next;  
    cur.next = node;  
}
2. 查(2道)

2.1 查找是否包含关键字key是否在单链表当中

【代码逻辑】

  1. 找思路:定义cur从head开始遍历链表;如果cur的val和key相同,return true
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:无

【代码实现】

public boolean contains(int key) {  
    ListNode cur = this.head;  
    while (cur != null) {  
        if (cur.val == key) {  
            return true;  
        }  
        cur = cur.next;  
    }  
    return false;  
}

2.2 得到单链表的长度

【代码逻辑】

  1. 找思路:遍历链表,每次count++,return count
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:无

【代码实现】

public int size() {  
    ListNode cur = this.head;  
    int count = 0;  
    while (cur != null) {  
        count++;  
        cur = cur.next;  
    }  
    return count;  
}
3. 删(3道)

【需考虑】

  1. index合法性
  2. 分开讨论删头节点还是其他节点,不同情况分开讨论
  3. 若cur.val存的是引用类型,也需要置为null
    3.1 删除第一次出现关键字为key的节点

【代码逻辑】

  1. 找思路:遍历链表,找key;找key的前驱;使key的前驱的next=key后一个节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:链表是否为空;分开讨论删头节点和其他节点;前驱合法性(是否为空)

【代码实现】

//找前驱
private ListNode findPrev(int key) {  
    ListNode cur = this.head;  
    while (cur != null) {  
        if (cur.next.val == key) {  
            return cur;  
        }  
        cur = cur.next;  
    }  
    return null;  
}

public void remove(int key) {  
    //链表若为空,删除失败  
    if (this.head == null) {  
        return;  
    }
    //删除头节点  
    if (this.head.val == key) {  
        this.head = this.head.next;  
        return;    }  
    //找前驱  
    ListNode prev = findPrev(key);  
    //为空,说明没有要删除的节点  
    if (prev == null) {  
        throw new RuntimeException();  
    }  
    //删除  
    ListNode cur = prev.next;  
    prev.next = cur.next;
}

3.2 删除所有值为key的节点

【代码逻辑】

  1. 找思路:遍历链表;找key;找key的前驱;使key的前驱的next=key后一个节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:判断头节点是否为空;分开讨论删头节点和其他节点;

【代码实现】推荐方式二

public void removeAllKey(int key) {  

    ListNode cur = this.head;  
    //方式一  
    ListNode cur = this.head.next;  
    ListNode prev = this.head; 
    //遍历链表 
    while (cur != null) {  
        //删 除了头节点以外的其他节点
        if (cur.val == key) {  
            prev.next = cur.next;  
            cur = cur.next;  
        }else {    //cur和prev往下一个节点走
            prev = prev.next;  
            cur = cur.next;  
        }  
        //删头结点
        if (this.head.val == key) {  
            this.head = this.head.next;  
    }  

    //方式二
    //遍历链表  
    while (cur != null) { 
        //链表若为空,删除失败  
        if (this.head == null) {  
            return;  
        } 
        //当节点值为key  
        if (cur.val == key)  {  
            //删头结点  
            if (cur == this.head) {  
                this.head = this.head.next;  
            }else {   //删 除了头节点以外的其他节点  
                ListNode prev = findPrev(key);  
                prev.next = cur.next;  
            }  
        }  
        //cur往下一个节点走  
        cur = cur.next;  
    }  
  
}

3.3 清空单链表

【代码逻辑】

  1. 找思路:遍历链表;将链表的每个节点的next域置为null
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:若cur.val存的是引用类型,也需要置为null

【代码实现】

public void clear() {  
    ListNode cur = this.head;  
    ListNode curNext = cur.next;  
    while (cur != null){  
        //若cur.val存的是引用类型,也需要置为null  
        //cur.val = null;        cur.next = null;  
        cur = curNext;  
    }  
    this.head = null;  
}

4. 实现双向链表CRUD

在这里插入图片描述

4.1 题目

【复制到IList接口内】

//头插法
void addFirst(int data);
//尾插法
void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
void addIndex(int index,int data);
//查找是否包含关键字key是否在单链表当中
boolean contains(int key);
//删除第一次出现关键字为key的节点
void remove(int key);
//删除所有值为key的节点
void removeAllKey(int key);
//得到单链表的长度
int size();
//清空链表
void clear();
//打印链表
void display();

4.2 实现LinkList

【前期准备】

  1. 准备链表的三个域:数据域val和next域和prev域,提供val的构造方法
  2. 准备链表头和尾:head和Last
  3. 创建链表(用头插/尾插创建链表)

【代码实现】

public class MyLinkList implements IList{  
  
    //内部类定义节点,每个节点都有val next prev  
    static class ListNode {  
        public int val;  
        public ListNode next;  
        public ListNode prev;  
  
        public ListNode(int val) {  
            this.val = val;  
        }  
    }  
    private ListNode head;  
    private ListNode Last;
}
0. 打印链表

【代码逻辑】

  1. 找思路:链表是通过每个节点的next域/prev域来遍历的,当节点为null时,遍历完成
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:无

【代码实现】

public void display() {  
    //cur指向头节点,通过next遍历链表
    ListNode cur = this.head;  
    //通过next遍历链表
    while (cur !=null) {  
        System.out.print(cur.val+" ");  
        cur = cur.next;  
    }  
    System.out.println();  
}
1. 增(3道)

【需考虑】

  1. index合法性
  2. 判断链表是否为空
  3. 插入时分开讨论,头插,尾插,中间插

1.1 头插法

【代码逻辑】

  1. 找思路:创建节点;新节点和头节点衔接;更换头节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:判断链表是否为空

【代码实现】

public void addFirst(int data) {  
    ListNode node = new ListNode(data);  
    //判断链表是否为空  
    if (this.head == null) {  
        //将头节点设为node  
        this.head = node;  
        //将尾结点设为node  
        this.Last = node;  
        return;
    }
    //头节点和node连接  
    node.next = this.head;  
    //更换头节点  
    this.head = node;  
}

1.2 尾插法

【代码逻辑】

  1. 找思路:创建节点;尾节点和新节点衔接;更换尾节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:无

【代码实现】

public void addLast(int data) {  
    ListNode node = new ListNode(data);  
    //如果链表为空
    if (this.head == null) {  
        this.head = node;  
        this.Last = node;  
        return;  
    }  
    //尾结点和node连接  
    Last.next = node;  
    node.prev = Last;  
    //更换尾结点  
    Last = Last.next;  
}

1.3 任意位置插入,第一个数据节点为0号下标

【代码逻辑】

  1. 找思路:创建节点;更改node前一个;更改node信息;更改node后一个
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码逻辑严谨性:检查index位置合法性;判断链表是否为空;

【代码实现】

//找前驱prev
private ListNode findIndex(int index) {  
    ListNode cur = this.head;  
    while(index > 0) {  
        cur = cur.next;  
        index--;  
    }  
    return cur;  
}  
//判断链表是否为空
private boolean isEmpty() {  
    return this.head == null;  
}
//检查index位置合法性
private int checkOnAddIndex(int index) {  
    if (index >= 0 && index <= size()) {  
        return 0;  
    }  
    return -1;  
}

public void addIndex(int index, int data) {  
    //检查index位置合法性  
    if (checkOnAddIndex(index) == -1) {  
        System.out.println("位置不合法");  
        return;    }  
    ListNode node = new ListNode(data);  
    ListNode in = findIndex(index);  
    //头插法  
    if (in == this.head) {  
        addFirst(data);  
    }else if (in == this.Last) {   //尾插法  
        addLast(data);  
    }else {    //中间插  
        //更改node前一个  
        in.prev.next = node;  
        //更改node信息  
        node.next = in;  
        node.prev = in.prev;  
        //改in的prev  
        in.prev = node;  
    }  
}
2. 查(2道)

2.1 查找是否包含关键字key是否在单链表当中

【代码逻辑】

  1. 找思路:定义cur从head开始遍历链表;如果cur的val和key相同,return true
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:无

【代码实现】

public boolean contains(int key) {  
    ListNode cur = this.head;  
    while (cur != null) {  
        //如果cur.val等于key
        if (cur.val == key) {  
            return true;  
        }  
        cur = cur.next;  
    }  
    return false;  
}

2.2 得到单链表的长度

【代码逻辑】

  1. 找思路:遍历链表,每次count++,return count
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:无

【代码实现】

public int size() {  
    ListNode cur = this.head;  
    int count = 0;  
    while (cur != null) {  
        count++;  
        cur = cur.next;  
    }  
    return count;  
}
3. 删(3道)

【需考虑】

  1. index合法性
  2. 分开讨论删头节点还是其他节点,不同情况分开讨论
  3. 若cur.val存的是引用类型,也需要置为null

3.1 删除第一次出现关键字为key的节点

【代码逻辑】

  1. 找思路:遍历链表,找key;找key的前驱;使key的前驱的next=key后一个节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:链表是否为空;分开讨论删头节点和其他节点;前驱合法性(是否为空)

【代码实现】

public void remove(int key) {  
    ListNode cur = this.head;  
  
    //遍历链表  
    while (cur != null) {  
  
        //该if..else  找 要删除的节点  
        //要进行删除的节点  
        if (cur.val == key) {  
  
            //该if..else  区分 删头节点或其他节点  
            //删除的是头节点  
            if (cur == this.head) {  
                //头节点往后移动,完成一半的删除操作  
                this.head = this.head.next;  
  
                //该if..else  区分节点数 1个或多个  
                //如果头节点往后移动后为空,说明链表原来只有一个节点,删除原来唯一的节点后链表为空  
                if (this.head == null) {  
                    //链表为空,头节点虽然指向为空,但Last还是指向原节点,需要把Last也置为null才算完成删除  
                    this.Last = null;  
                }else {  //如果头节点往后移动后为空,说明链表不只有一个节点  
                    //将新的头节点前驱设为null  
                    this.head.prev = null;  
                }  
  
            }else {   //删除 除了头节点以外的其他节点  
                //删除的不是尾节点时,进行cur前一个节点和后一个节点的衔接  
                if (cur != this.Last) {  
                    //cur处在尾节点时,cur的下一个为null,会有空指针异常  
                    cur.next.prev = cur.prev;  
                }else {   //删除的是尾节点时,尾节点前移  
                    this.Last = this.Last.prev;  
                    //this.Last.next = null; 等价于下面那行cur.prev.next = cur.next;  
                }  
                //cur的前一个的next设为cur的下一个节点  
                cur.prev.next = cur.next;  
  
            }  
  
            //因为只删除一次,所以删完要return  
            return;  
        }else {  
            //不是要删除的节点,继续往后走  
            cur = cur.next;  
        }  
    }  
}

3.2 删除所有值为key的节点
其与“删除第一次出现关键字为key的节点”不同的是:

  1. 因为删除许多次,所以删完一次后不用return,继续循环
  2. 删去最后一个else

【代码逻辑】

  1. 找思路:遍历链表;找key;找key的前驱;使key的前驱的next=key后一个节点
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:区分删头节点,还是删除了头节点以外的其他节点;区分节点个数,只有一个或多个

【代码实现】

public void removeAllKey(int key) {  
    ListNode cur = this.head;  
  
    //遍历链表  
    while (cur != null) {  
  
        //不要用if..else!!!!  找 要删除的节点  
        //要进行删除的节点  
        if (cur.val == key) {  
  
            //该if..else  区分 删头节点或其他节点  
            //删除的是头节点  
            if (cur == this.head) {  
                //头节点往后移动,完成一半的删除操作  
                this.head = this.head.next;  
  
                //该if..else  区分节点数 1个或多个  
                //如果头节点往后移动后为空,说明链表原来只有一个节点,删除原来唯一的节点后链表为空  
                if (this.head == null) {  
                    //链表为空,头节点虽然指向为空,但Last还是指向原节点,需要把Last也置为null才算完成删除  
                    this.Last = null;  
                }else {  //如果头节点往后移动后为空,说明链表不只有一个节点  
                    //将新的头节点前驱设为null  
                    this.head.prev = null;  
                }  
  
            }else {   //删除 除了头节点以外的其他节点  
                //删除的不是尾节点时,进行cur前一个节点和后一个节点的衔接  
                if (cur != this.Last) {  
                    //cur处在尾节点时,cur的下一个为null,会有空指针异常  
                    cur.next.prev = cur.prev;  
                }else {   //删除的是尾节点时,尾节点前移  
                    this.Last = this.Last.prev;  
                    //this.Last.next = null; 等价于下面那行cur.prev.next = cur.next;  
                }  
                //cur的前一个的next设为cur的下一个节点  
                cur.prev.next = cur.next;  
            }  
  
            //不同点1:因为删除许多次,所以删完不用return  
            //return;        }  
        //不同点2:删去else  
        cur = cur.next;  
  
    }  
}

3.3 清空单链表

【代码逻辑】

  1. 找思路:遍历链表;将链表的每个节点的next域和prev域置为null;将头节点和尾节点置为null
  2. 写代码框架:无
  3. 填充代码:无
  4. 完善代码严谨性:若cur.val存的是引用类型,也需要置为null

【代码实现】

public void clear() {  
    ListNode cur = this.head;  
    while (cur != null) {  
        cur.prev = null;  
        cur.next = null;  
        cur = cur.next;  
        //如果引用类型,cur.val也需要置为null  
        //cur.val = null;   
    }  
    this.head = null;  
    this.Last = null;  
}

5. 面试题

提问形式
  1. ArrayList(顺序表)和LinkList(链表)的区别是什么?
  2. 数组和链表的区别?

【答】若经常给定下标查询的话,适合用顺序表;若经常进行插入/删除操作的话,用链表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值