Algorithms_基础数据结构(03)_线性表之链表_双向链表

简介: Algorithms_基础数据结构(03)_线性表之链表_双向链表

20191216220326136.png

大纲图


20191229231819317.png


双向链表

Algorithms_基础数据结构(02)_链表&链表的应用案例之单向链表中梳理了 单向链表的基本操作,接下来我们继续来看下双向链表吧。


双向链表的基本结构


20200103102453372.png


单向链表只有一个方向,结点只有一个后继指针next指向后面的结点。


双向链表,顾名思义,它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点。


双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。


虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。那相比单链表,双向链表适合解决哪种问题呢?


-----> B+Tree:Mysql索引 叶子节点 双向链表


双向链表的基本操作

头插


20200103102538501.png


尾插


20200103155130564.png



中间部位插入


20200103155156449.png



删除头部



20200103164237376.png


删除尾部


20200103164201937.png


删除中间位置的数据


20200103164249476.png

查找


通常,双向链表同单链表一样,都仅有一个头指针。所以双链表查找指定元素的实现同单链表类似,都是从表头依次遍历表中元素,直到找到对应的元素为止。


更新

更改双链表中指定结点数据域的操作那必须要先查找到该节点,因此是在查询的基础上完成的。------>即:通过遍历找到存储有该数据元素的结点,直接更改其数据域即可。


Code

/**
 * @author 小工匠
 * @version v1.0
 * @create 2020-01-03 06:08
 * @motto show me the code ,change the word
 * @blog https://artisan.blog.csdn.net/
 * @description
 **/
public class ArtisanDoubleLinkedList {
    private ArtisanNode head; // head节点
    private ArtisanNode tail; // tail节点  为了方便直接获取tail节点,省去每一次都要遍历的操作
    private int size; // 链表元素数量
    /**
     * 双向链表初始化
     */
    public ArtisanDoubleLinkedList() {
        this.head = null;
        this.tail = null;
    }
    /**
     * 头插
     * @param data
     */
    public  void add2Head(Object data) {
        ArtisanNode node = new ArtisanNode(data); // 新的Node
        if (this.head == null) { // 如果head节点为null,  head和tail节点均为这个新的node节点
            this.tail = node;
        } else {// 将原来的头节点的前驱节点指向node, 将新节点的后驱节点指向head
            this.head.pre = node;
            node.next = head;
        }
        this.head = node; // 将新的节点置为head节点
        size++;
    }
    /**
     * 尾插 (低效)
     *
     * @param data
     */
    public void add2Tail(Object data) {// 从头部遍历,找到最后的节点,然后加到尾部
        ArtisanNode node = new ArtisanNode(data); // 要加入的节点
        ArtisanNode currentNode = head;
        if (currentNode == null){
            add2Head(data);
        }
        while(currentNode !=null){
            if (currentNode.next == null){ // 说明找到了当前的tail节点
                currentNode.next = node ;// 将当前tail节点的next指针指向新的tail节点
                node.pre = currentNode; //新的tail节点的pre指向当前tail节点节点
                this.tail = node;
                break;
            }else{
                currentNode = currentNode.next;
            }
        }
        size++;
    }
   /**
     * 尾插 (利用tail 无需遍历 效率更高)
     *
     * @param data
     */
    public void add2Tail2(Object data) {// 已经设置tail了,直接用即可,效率更高
        ArtisanNode node = new ArtisanNode(data); // 要加入的节点
        if (this.head == null){
            add2Head(data);
        }else {
            tail.next = node;
            node.pre = tail;
            tail = node;
        }
    }
    /**
     *
     * @param postition
     * @param data
     */
    public void add2Nth(int postition ,Object data) {
        ArtisanNode newNode = new ArtisanNode(data); // 新的Node
        ArtisanNode currentNode = head;
        if (postition == 0 ){ // 如果是0 ,添加到头节点
            add2Head(data);
        }else {
            for (int i = 1; i < postition; i++) { // 找到要插入的位置的前面的节点
                currentNode = currentNode.next;
            }
            // 与后继节点建立双层逻辑关系
            newNode.next = currentNode.next;
            currentNode.next.pre = newNode;
            // 与前置节点建立双层逻辑关系
            currentNode.next = newNode;
            newNode.pre = currentNode;
        }
        size++;
    }
    /**
     * 根据value 查找元素
     * @param data
     * @return
     */
    public ArtisanNode find(Object data){ // 从头遍历
        ArtisanNode currentNode = head;
        while(currentNode != null){
            if (data.equals(currentNode.data)){
                printPreAndNextInfo(currentNode);
                break;
            }else{
                currentNode = currentNode.next;
            }
        }
        return currentNode;
    }
    /**
     * 删除头部节点
     */
    public  void deleteHead(){
        this.head = this.head.next; // 将当前头节点的下一个节点置为头节点
        this.head.pre = null; // 将前置节点置为null
        size--;
    }
    /**
     * 删除尾部节点
     */
    public void deleteTail(){
        ArtisanNode currentNode = this.head;
        ArtisanNode previousNode = null;
        while (currentNode != null){
            if (currentNode.next == null){
                currentNode.pre = null;// 最后一个节点的pre置为置为null
                previousNode.next = null;// 前置节点的next指针置为null
                this.tail = previousNode; // 将当前节点的前一个节点置为tail节点
            }else { // 如果当前节点的next指针指向不为空,则把下个节点置为当前节点,继续遍历
                previousNode = currentNode;// 保存上一个节点的信息
                currentNode = currentNode.next;
            }
        }
    }
    /**
     * 删除指定位置的节点
     * @param position
     */
    public ArtisanNode deleteNth(int position){
        ArtisanNode currentNode = this.head;
        if (position == 0 ){
            deleteHead();
        }else {
            for (int i = 1 ; i < position ; i++){// 找到要删除节点的前一个节点
                currentNode = currentNode.next;
            }
            currentNode.next.next.pre = currentNode; // 将  要删除节点的后一个节点的前驱节点 指向 当前节点(要删除的节点的前一个节点)
            currentNode.next = currentNode.next.next; // 将 要删除节点的前一个节点的next指针指向 要删除节点的后一个节点
        }
        size--;
        return currentNode.next ; // 返回删除的节点
    }
    /**
     * 获取tail节点
     * @return tail节点
     */
    public ArtisanNode getTail(){
        System.out.println("tail节点的值为:" + this.tail.data );
        return this.tail;
    }
    /**
     * 获取head节点
     * @return head节点
     */
    public ArtisanNode getHead(){
        System.out.println("head节点的值为:" + this.head.data );
        return this.head;
    }
    /**
     * 打印链表中的数据
     */
    public void print() {
        ArtisanNode currentNode = this.head;// 从head节点开始遍历
        while (currentNode != null) { // 循环,节点不为空 输出当前节点的数据
            System.out.print(currentNode.data + " -> ");
            currentNode = currentNode.next; // 将当前节点移动到下一个节点,循环直到为null
        }
        System.out.print("null");
        System.out.println();
    }
    /**
     * 打印前后节点信息
     * @param currentNode
     */
    private void printPreAndNextInfo(ArtisanNode currentNode) {
        System.out.println("当前节点:" + currentNode.data);
        if (currentNode.pre != null){
            System.out.println("当前节点【" + currentNode.data + "】的前驱节点:" + currentNode.pre.data);
        }else{
            System.out.println("当前节点【"+ currentNode.data + "】为head节点");
        }
        if (currentNode.next != null){
            System.out.println("当前节点【"+ currentNode.data + "】的后继节点:" + currentNode.next.data);
        }else{
            System.out.println("当前节点【"+ currentNode.data + "】为tail节点");
        }
    }
    public static void main(String[] args) {
        ArtisanDoubleLinkedList doubleLinkedList = new ArtisanDoubleLinkedList();
        doubleLinkedList.add2Head("artisanData96");
        doubleLinkedList.add2Head("artisanData97");
        doubleLinkedList.add2Head("artisanData99");
        doubleLinkedList.add2Head("artisanData98");
        doubleLinkedList.getTail();
        doubleLinkedList.add2Tail("artisanData100");
        doubleLinkedList.getTail();
        doubleLinkedList.print();
        doubleLinkedList.getHead();
//        doubleLinkedList.add2Nth(2,"addedDataByPos");
//        doubleLinkedList.add2Tail2(1);
//        doubleLinkedList.add2Tail2(2);
//        doubleLinkedList.add2Tail2(3);
//        doubleLinkedList.add2Tail2(4);
//        doubleLinkedList.print();
//
//        System.out.println("tail:" + doubleLinkedList.tail.data);
//
//        doubleLinkedList.find("artisanData98");
//        doubleLinkedList.deleteHead();
//        doubleLinkedList.print();
//        doubleLinkedList.find("artisanData99");
//        System.out.println("被删除节点:" + doubleLinkedList.deleteNth(1).data);
//        doubleLinkedList.print();
//        doubleLinkedList.find("artisanData96");
    }
    /**
     * 双向链表中的节点
     */
    class ArtisanNode {
        ArtisanNode pre; // 前驱结点
        Object data; // 数据
        ArtisanNode next;// 后继节点
        public ArtisanNode(Object data) {
            this.data = data;
        }
    }
}

总结


2020010300304875.png


重要区别:


1.数组简单易用,在实现上使用的是连续的内存空间,可以借助CPU的缓存机制,预读数组中的数据,所以访问效率更高。


2.链表在内存中并不是连续存储,所以对CPU缓存不友好,没办法有效预读。


3.数组的缺点是大小固定,一经声明就要占用整块连续内存空间。如果声明的数组过大,系统可能没有足够的连续内存空间分配给它, 导致“内存不足(out ofmemory)”。如果声明的数组过小,则可能出现不够用的情况。注意下标越界的问题。


4.动态扩容:数组需再申请一个更大的内存空间,把原数组拷贝进去,非常费时。链表本身没有大小的限制,天然地支持动态扩容,使用的时候也需要考虑占用内存的问题。

相关文章
|
10月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
375 30
|
10月前
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
457 25
|
11月前
|
存储 算法 测试技术
【C++数据结构——线性表】求集合的并、交和差运算(头歌实践教学平台习题)【合集】
本任务要求编写程序求两个集合的并集、交集和差集。主要内容包括: 1. **单链表表示集合**:使用单链表存储集合元素,确保元素唯一且无序。 2. **求并集**:遍历两个集合,将所有不同元素加入新链表。 3. **求交集**:遍历集合A,检查元素是否在集合B中存在,若存在则加入结果链表。 4. **求差集**:遍历集合A,检查元素是否不在集合B中,若满足条件则加入结果链表。 通过C++代码实现上述操作,并提供测试用例验证结果。测试输入为两个集合的元素,输出为有序集合A、B,以及它们的并集、交集和差集。 示例测试输入: ``` a c e f a b d e h i ``` 预期输出:
312 7
|
11月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
517 5
|
11月前
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】顺序表的基本运算(头歌实践教学平台习题)【合集】
本文档介绍了线性表的基本运算任务,涵盖顺序表和链表的初始化、销毁、判定是否为空、求长度、输出、查找元素、插入和删除元素等内容。通过C++代码示例详细展示了每一步骤的具体实现方法,并提供了测试说明和通关代码。 主要内容包括: - **任务描述**:实现顺序表的基本运算。 - **相关知识**:介绍线性表的基本概念及操作,如初始化、销毁、判定是否为空表等。 - **具体操作**:详述顺序表和链表的初始化、求长度、输出、查找、插入和删除元素的方法,并附有代码示例。 - **测试说明**:提供测试输入和预期输出,确保代码正确性。 - **通关代码**:给出完整的C++代码实现,帮助完成任务。 文档
362 5
|
12月前
|
数据库
数据结构中二叉树,哈希表,顺序表,链表的比较补充
二叉搜索树,哈希表,顺序表,链表的特点的比较
数据结构中二叉树,哈希表,顺序表,链表的比较补充
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
295 59
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
127 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
526 77

热门文章

最新文章