双向链表

[!Tip]

本节源代码见Github链接🔗


概述

在双向链表中,每个元素结点保存了指向前驱结点的指针。因此对于任意一个结点,都可以从两个方向进行操作。 双向链表的数据结构如下图所示: 双向链表的类型声明方式如下所示:

【👉🏻>>点击展开查看代码】
        
/**  
 * 双向链表元素结点数据结构  
 *  
 * @className: DllNode  
 * @author: Max Solider  
 * @date: 2022-10-08 17:02  
 */
 public class DllNode {  

    /**  
     * 结点数据  
     */  
    private int data;  

    /**  
     * 前驱结点  
     */  
    private DllNode previous;  

    /**  
     * 后继结点  
     */  
    private DllNode next;  

    public DllNode(int data) {  
        this.data = data;  
    }  

    public int getData() {  
        return data;  
    }  

    public void setData(int data) {  
        this.data = data;  
    }  

    public DllNode getPrevious() {  
        return previous;  
    }  

    public void setPrevious(DllNode previous) {  
        this.previous = previous;  
    }  

    public DllNode getNext() {  
        return next;  
    }  

    public void setNext(DllNode next) {  
        this.next = next;  
    }  
}
        
    

优缺点及相关应用场景

优点

  • 对于链表中的任意结点,可以快速后退到前驱结点。

缺点

  • 每个结点需要额外的内存开销,用于保存前驱结点;
  • 对结点的插入和删除更加费时(需要更多的指针操作)。

应用


基本操作

对双向链表的基本操作包括:

  • 在链表中插入元素结点;
  • 删除链表中的元素结点。

在链表中插入元素

常见操作

  1. 在链表开头前插入一个新结点;
  2. 在链表尾后插入一个新结点;
  3. 在链表中间任意位置插入一个新结点。

在表头前插入一个新结点

核心思路
在双向链表的表头前插入一个新结点,包括以下2步:

  1. 将原链表头的前驱指针指向新结点;
  2. 将新结点的后继指针指向原表头结点,并将新结点的前驱指针赋值为NULL。

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 在双向链表头前插入新结点  
 *  
 * @param headNode 原链表头结点  
 * @param newNode 新结点  
 * @return DllNode  
 * @author: Max Solider  
 * @date: 2022/10/8 17:12  
 */
 DllNode insertHead(DllNode headNode, DllNode newNode) {  
    if (newNode == null) {  
        return headNode;  
    }  
    if (headNode == null) {  
        return newNode;  
    }  
    // 改变原头结点的前驱指针  
    headNode.setPrevious(newNode);  
    // 改变新结点的后继指针  
    newNode.setNext(headNode);  
    headNode = newNode;  
    return headNode;  
}
        
    

时间复杂度
时间复杂度是O(1)
空间复杂度
无需额外空间。

在表尾插入一个新结点

核心思路
在表尾后插入一个新结点,包含以下3步:

  1. 遍历找到原表尾结点;
  2. 将原表尾结点的后继指针指向新结点;
  3. 将新结点的前驱指针指向原表尾结点,并将后继指针指向NULL。 双向链表表尾插入新结点.png

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 在链表尾后插入新结点  
 *  
 * @param headNode 链表头结点指针  
 * @param newNode  新结点  
 * @return DllNode 新的表头指针  
 * @author: Max Solider  
 * @date: 2022/10/8 17:21  
 */
 DllNode insertTail(DllNode headNode, DllNode newNode) {  
    if (headNode == null) {  
        return newNode;  
    }  
    if (newNode == null) {  
        return headNode;  
    }  
    DllNode currentNode = headNode;  
    while (currentNode.getNext() != null) {  
        currentNode = currentNode.getNext();  
    }  
    currentNode.setNext(newNode);  
    newNode.setPrevious(currentNode);  
    newNode.setNext(null);  
    return headNode;  
}
        
    

时间复杂度
由于需要遍历链表,时间复杂度为O(n)
空间复杂度
需要存储一个临时变量,空间复杂度为O(1)

在链表中间插入一个新结点

核心思路
在链表中间任意位置插入一个新结点,包含以下6步:

  1. 判断插入位置合法性;
  2. 判断是否在表头插入;
  3. 遍历找到待插入位置;
  4. 找到待插入位置原结点的前驱结点,并将前驱结点的后继指针指向新结点;
  5. 将原结点的前驱指针指向新结点;
  6. 将新结点的前驱指针指向原前驱结点,将新结点的后继指针指向原结点。 双向链表任意位置插入新结点.png

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 双向链表中任意位置插入结点  
 *  
 * @param headNode 链表头指针  
 * @param newNode  新结点  
 * @param position 待插入位置  
 * @return DllNode  
 * @author: Max Solider  
 * @date: 2022/10/8 17:47  
 */
 DllNode insert(DllNode headNode, DllNode newNode, int position) {  
    if (headNode == null) {  
        headNode = newNode;  
        return headNode;  
    }  
    if (position < 1) {  
        System.out.println("The position of node to insert is invalid. The position must be greater than 0.");  
        return headNode;  
    }  
    if (position == 1) {  
        // 改变原头结点的前驱指针  
        headNode.setPrevious(newNode);  
        // 改变新结点的后继指针  
        newNode.setNext(headNode);  
        headNode = newNode;  
        return headNode;  
    }  
    // 先找到待插入位置的原结点  
    int count = 1;  
    DllNode currentNode = headNode;  
    DllNode previousNode = currentNode.getPrevious();  
    while (count != position) {  
        if (currentNode == null) {  
            System.out.println("The position of node to insert is invalid. The position must be less than " + (count + 1));  
            return headNode;  
        }  
        previousNode = currentNode;  
        currentNode = currentNode.getNext();  
        count++;  
    }  
    // 改变原结点前驱结点的后继指针  
    previousNode.setNext(newNode);  
    // 改变新结点的前驱指针和后继指针  
    newNode.setPrevious(previousNode);  
    newNode.setNext(currentNode);  
    // 如果后继结点不是null,则修改后继结点的前驱指针  
    if (currentNode != null) {  
        currentNode.setPrevious(newNode);  
    }  
    return headNode;  
}
        
    

时间复杂度
因为需要遍历链表,时间复杂度是O(n)
空间复杂度
因为需要保存临时变量,空间复杂度为O(1)


从链表中删除元素

常见操作

  1. 删除链表的表头;
  2. 删除链表的表尾;
  3. 删除链表中任意结点。

删除表头结点

核心思路
删除表头结点,包含以下4步:

  1. 将表头指针指向第二个结点;
  2. 将第二个结点的前驱指针指向NULL;
  3. 将原表头结点的后继指针指向NULL;
  4. 移除原表头结点。 删除双向链表表头结点.png

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 删除表头结点  
 *  
 * @author: Max Solider  
 * @date: 2022/10/8 19:38  
 * @param headNode  
 * @return org.example.linkedlist.dll.DllNode  
 */
 DllNode deleteHead(DllNode headNode) {  
    if (headNode == null) {  
        return headNode;  
    }  
    // 如果链表只有一个元素  
    if (headNode.getNext() == null) {  
        headNode = null;  
        return headNode;  
    }  
    // 将表头指针指向第二个结点  
    DllNode newHead = headNode.getNext();  
    // 修改将原表头结点的后继指针  
    headNode.setNext(null);  
    // 修改新表头结点的前驱指针  
    newHead.setPrevious(null);  
    headNode = newHead;  
    return headNode;  
}
        
    

时间复杂度
因为无需遍历链表,时间复杂度是O(1)
空间复杂度
因为需要保存临时变量,空间复杂度为O(1)

删除表尾结点

核心思路
删除表尾结点,包含以下4步:

  1. 找到原表尾结点;
  2. 找到原表尾结点的前驱结点,并将它的后继指针指向新结点;
  3. 将新结点的前驱指针指向原表尾结点的前驱结点,并将后继指针指向NULL;
  4. 将原表尾结点的前驱结点指向NULL。 双向链表删除表尾结点.png

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 删除表尾结点  
 *  
 * @author: Max Solider  
 * @date: 2022/10/8 20:38  
 * @param headNode  
 * @return org.example.linkedlist.dll.DllNode  
 */
 DllNode deleteTail(DllNode headNode) {  
    if (headNode == null || headNode.getNext() == null) {  
        headNode = null;  
        return headNode;  
    }  
    // 遍历找到表尾结点  
    DllNode tailNode = headNode;  
    while (tailNode.getNext() != null) {  
        tailNode = tailNode.getNext();  
    }  
    tailNode.getPrevious().setNext(null);  
    tailNode.setPrevious(null);  
    return headNode;  
}
        
    

时间复杂度
时间复杂度是O(n),用于遍历链表。
空间复杂度
空间复杂度为O(1),用于保存临时变量。

删除链表中间的结点

核心思路
删除链表中间任意结点,包括以下5步:

  1. 判断删除位置合法性;
  2. 找到待删除位置;
  3. 找到待删除结点的前驱结点,将它的后继指针指向待删除结点的后继结点;
  4. 找到待删除结点的后继结点,将它的前驱指针指向待删除结点的前驱结点;
  5. 移除待删除结点,将它的前驱指针和后继指针都指向NULL。 删除双向链表中任意结点.png

实现代码

【👉🏻>>点击展开查看代码】
        
/**  
 * 删除任意结点  
 *  
 * @author: Max Solider  
 * @date: 2022/10/8 20:58  
 * @param headNode  
 * @param position  
 * @return org.example.linkedlist.dll.DllNode  
 */
 DllNode delete(DllNode headNode, int position) {  
    if (headNode == null) {  
        return headNode;  
    }  
    if (position < 1) {  
        System.out.println("The position of node to delete is invalid. The position must be greater than 0");  
        return headNode;  
    }  
    // 找到待删除结点  
    DllNode deleteNode = headNode;  
    int count = 1;  
    while (count != position) {  
        deleteNode = deleteNode.getNext();  
        count++;  
        if (deleteNode == null) {  
            System.out.println("The position of node to delete is invalid. The position must be less than " + count);  
            return headNode;  
        }  
    }  
    // 更新待删除结点的前驱结点信息  
    if (deleteNode.getPrevious() != null) {  
        deleteNode.getPrevious().setNext(deleteNode.getNext());  
    } else {  
        // 如果删除的是表头结点,需要更新head指针  
        headNode = deleteNode.getNext();  
    }  
    // 更新待删除结点的后继结点信息  
    if (deleteNode.getNext() != null) {  
        deleteNode.getNext().setPrevious(deleteNode.getPrevious());  
    }  
    // 更新待删除结点信息  
    deleteNode.setPrevious(null);  
    deleteNode.setNext(null);  
    return headNode;  
}
        
    

时间复杂度
时间复杂度是O(n),用于遍历链表。
空间复杂度
空间复杂度为O(1),用于保存临时变量。


© MaxSolider all right reserved,powered by Gitbook文件修订时间: 2022-10-12 23:27:42

results matching ""

    No results matching ""