• 数据结构之链表-动图演示


    链表简介

    链表是很常见的数据结构,由一个个节点组成,每个节点中储存着数据和指针(地址引用),指针负责节点间的连接。

    它是一种线性表,线性表有两种存储方式:顺序存储和链式存储。链表属于链式存储,顺序由元素间的指针决定,元素在内存中非连续存放,且链表长度可以改变。数组是顺序存储的线性表,元素在内存中连续存放的,且数组创建时大小已固定。

    链表可以用来实现栈和队列数据结构(栈和队列可理解为逻辑类数据结构,链表属于存储类数据结构),实现缓存LRU算法,Java类库也使用了链表(如,LinkedList,LinkedHashMap)等。链表的形式有很多,常用的有单向链表、双向链表、循环链表 ...

    单向链表

    单链表中的节点分两部分,分别是数据(data)和指向下一个节点的地址(next),尾节点(tail)的next指向null。单向链表只能从头到尾一个方向遍历,查找节点时需要从头节点(head)开始向下查找。
    插入节点首先遍历查找到插入的位置,然后将当前插入节点的next指向下一节点,上一节点的next指向当前插入节点。删除节点同样从头遍历找到要删除的节点,然后将当前删除节点的上一个节点next指向当前删除节点的下一个节点。

    单向链表

    节点的伪代码:

    class Node<E>{
        private E item;
        private Node<E> next; // 如果是尾节点,next指向null
        Node(E data, Node<E> next) {
            this.item = data;
            this.next = next;
        }
        // ...
    }
    
    

    单向循环链表

    循环链表和非循环链表基本一样,区别是首尾节点连在了一起,最后一个节点的next指向头节点,形成了一个闭环。

    单向循环链表

    节点的伪代码:

    class Node<E>{
        private E item;
        // 如果是末尾节点,指向首节点的引用地址
        private Node<E> next;
        Node(E data, Node<E> next) {
            this.item = data;
            this.next = next;
        }
        // ...
    }
    
    

    双向链表

    顾名思义,与单向链表相比较,双向链表可以从头到尾或从尾到头两个方向来遍历数据。双向链表中的节点分三个部分,分别是指向上一个节点的地址(prev)和数据(data)以及指向下一个节点的地址(next),尾节点(tail)节点的next指向null,头节点(head)的prev指向null。
    增加和删除节点和单向链表同理,只是增加了修改prev地址的操作。

    双向链表

    节点的伪代码:

    class Node<E>{
        private E item;
        private Node<E> prev; // 头节点prev指向null
        private Node<E> next; // 尾节点next指向null
        Node(Node<E> prev, E data, Node<E> next) {
            this.item = data;
            this.prev = prev;
            this.next = next;
        }
       // ... 
    }
    
    

    双向循环链表

    尾节点的next指向头节点,头节点的prev指向尾节点,首尾节点连在一起形成闭环。

    双向循环链表

    节点的伪代码:

    class Node<E> { 
        private E item;
        // 如果是第一个节点,其一用指向末尾节点
        private Node<E> prev;
        // 如果是末尾节点,指向第一个节点的引用地址,形成一个环形
        private Node<E> next;
        Node(Node<E> prev, E data, Node<E> next) {
            this.item = data;
            this.prev = prev;
           this.next = next;
       }
       // ...
    }
    
    

    链表操作

    链表的增删改查操作。链表查找节点需要从头或者尾部(单向链表只能从头开始)开始查找,删除或插入节点先查找到节点,然后改变相关节点的指针指向即可。

    以双向链表为例:

    添加节点

    1. 头部添加节点

    添加节点

    伪代码:

    Node<E> head;
    Node<E> tail;
    int size;
    
    // 头部添加节点
    void addHead(E e) {
        Node<E> h = head;
        Node<E> newNode = new Node<>(null, e, h); // (Node<E> prev, E element, Node<E> next)
        head = newNode;
        if(h == null) { // 空链表
            tail = newNode;
         } else {
           h.prev = newNode;
        }   
       size++; // 记录长度
    }
    
    
    1. 尾部添加节点

    添加节点

    伪代码:

    void addTail(E e) {
        Node<E> t = tail;
        Node<E> newNode = new Node<>(t, e, null);
        tail = newNode;
        if(t == null) {
            head = newNode;
        } else {
           t.next = newNode;      
        }       
        size++;
    }
    
    
    1. 按位置插入节点

    添加节点

    伪代码:

    void add(int index, E element) {
            if (index == size) {
                // 直接在尾部添加节点
            } else {
               // 查找的节点
               Node<E> temp = null;
               if (index < (size >> 1)) {//由于双向链表,选择从离index位置最近端查找
                    Node<E> x = head;
                    for (int i = 0; i < index; i++) {
                        x = x.next;
                    }
                    temp = x;    
                } else {
                    Node<E> x = tail;
                    for (int i = size - 1; i > index; i--) {
                        x = x.prev;
                    }
                    temp = x;   
                }
                
                // 插入节点
                Node<E> pred = temp.prev;
                Node<E> newNode = new Node<>(pred, element, temp);
                temp.prev = newNode;
                if (pred == null) { // 查找到的节点为Head节点
                    head = newNode;
                } else {
                    pred.next = newNode;
                }   
            } 
            size++;
        }
    
    

    删除节点

    1. 删除头部节点

    删除节点

    伪代码:

    E removeHead() {
        Node<E> h = head;    
        if (h != null){
            E element = h.item;
            Node<E> next = h.next;
            head = next;
            if (next == null) {
                tail = null;
            } else {
                next.prev = null;
           }   
           size--; // 减少长度
           return element; // 返回删除元素
        }
        return null;
    }
    
    
    1. 删除尾部节点

    删除节点

    伪代码:

    E removeTail() {
        Node<E> t = tail;
        if (t != null) {
            E element = t.item;
            Node<E> prev = t.prev;
            tail = prev;
            if (prev == null) {
                head = null;
            } else {
               prev.next = null;
           }
           size--;
           return element;
        }
        return null; 
    }
    
    
    1. 按节点位置或值删除

    删除节点

    伪代码-按位置删除:

    E remove(int index) {
        // 根据index查找节点
        Node<E> temp = null;
        if (index < (size >> 1)) {
            Node<E> x = head;
            for (int i = 0; i < index; i++) {
                x = x.next;
            }
            temp = x;    
        } else {
            Node<E> x = tail;
            for (int i = size - 1; i > index; i--) {
                x = x.prev;
            }
            temp = x;   
        }
        // 删除节点
        E element = temp.item;
        Node<E> next = temp.next;
        Node<E> prev = temp.prev;
        if (prev == null) {
            head = next;
        } else {
            prev.next = next;
            temp.prev = null;
        }
        if (next == null) {
            tail= prev;
        } else {
            next.prev = prev;
            temp.next = null;
        }
        temp.item = null;
        size--; 
        return element;
    }
    
    

    查找节点

    1. 按位置或值查找节点

    查找节点

    伪代码-按位置索引查找:

    E get(int index) {
      Node<E> temp = null;
      if (index < (size >> 1)) { // 从近的一端开始查找
            Node<E> x = first;
            for (int i = 0; i < index; i++) {
                x = x.next;
            } 
            temp = x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--) {
                x = x.prev;
            } 
            temp = x;
        }
        return temp.item;
    }
    
    

    如果是单向链表只能从头部开始向后查找。

    更新节点

    更新节点首先查找到节点,然后修改节点data的指针。

    具体可参考LinkedList源码

    链表实现栈和队列

    栈和队列是一种对数据存取有严格顺序要求的线性数据结构,使用链表和数组都能实现。下面使用链表来实现栈和队列。

    栈只能从一端存取数据,遵循后进先出(LIFO)原则。进出栈的一端称为栈顶,另一封闭端称为栈底,数据进入栈称为入栈或压栈,取出数据称为出栈或弹栈。

    栈

    伪代码 - 基于双向链表实现简单的“栈”:

    class Stack<E> {
      
        // 返回栈顶元素值
        public E peek() {
            Node<E> h = head;
            return (h == null) ? null : h.item;
        }
        
        // 入栈
        public void push(E e) {
            addHead(e); // 在头部添加节点
        }
        
        // 出栈
        public E pop() {
            // 移除头部节点并返回值
            return removeHead(); 
        }
        
        // ...
        
        private static class Node<E> {
            E item;
            Node<E> next;
            Node<E> prev;
            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }  
    }
    
    

    队列

    队列是从两端存取数据,并且从一端进,从另一端出,遵循先进先出(FIFO)原则。队列进数据一端称为队尾,出数据端称为队头,数据进队列称为入队,取出队列称为出队。

    队列

    伪代码 - 基于链表实现“队列”:

    class Queue {
        // 入队
        public boolean offer(E e) {
            return addTail(e); 
        }
        
        // 出队
        public E poll() {
            return removeHead();
        }
        
        // 返回头元素值
        public E peek() {
            Node<E> h = head;
            return (h == null) ? null : h.item;
        }
        
        private static class Node<E> {
            E item;
            Node<E> next;
            Node<E> prev;
            Node(Node<E> prev, E element, Node<E> next) {
                this.item = element;
                this.next = next;
                this.prev = prev;
            }
        }   
    }
    
    

    快慢指针

    快慢指针是解决链表某些问题的常用方法,利用两个不同步频的指针fast指针和slow指针算法来解决很多问题,例如:

    查找未知长度的单向链表倒数第N个值

    由于链表长度未知,首先循环链表得到 length,然后再次循环链表到length-(N-1) 处得到元素。但是利用快慢指针来保持固定位置间隔,只需要循环一次链表即可查找到元素。

    查找倒数第N个值

    伪代码:

    public E getLastN(int n) {
    	Node<E> h = head;
    	if (h == null || n < 1) {
    		return null;
    	}
    	Node<E> fast = h; // 快
    	Node<E> slow = h; // 慢
    	int count = 1;
    	while ((fast = fast.next) != null) {
    		// 倒数第k个节点与倒数第1个节点相隔 n-1 个位置,因此fast先走 n-1 个位置
    		if (count++ > n - 1) {
    			slow = slow.next;
    		}
    	}
    	// 链表中的元素个数小于 n
    	if (count < n) {
    		return null;
    	}
     
    	return slow.item;
    }
    
    

    找到链表中间节点值

    使快指针移动步频是慢指针二倍,一次遍历即可快速找到中间节点。

    查找中间节点

    伪代码:

    public E getMiddle() {
    	Node<E> h = head;
    	if (h == null) {
    		return null;
    	}
    	Node<E> fast = h; // 快
    	Node<E> slow = h; // 慢
    	while (fast != null && fast.next != null) {
    		fast = fast.next.next;
    		// 链表长度为偶数会两个中间节点,返回第一个
    		if (fast != null) {
    			slow = slow.next;
    		}
    	}
    	return slow.item;
    }
    
    

    源码:https://github.com/newobjectcc/code-example/blob/master/basic/datastructure/Linked.java

    除此之外,还可以判断链表中是否有环等等问题,快慢指针在面试时可能会被问到,有兴趣朋友可以到网上找些链表的算法题。

    作者:陆十三
    转载请在明显位置注明出处!
  • 相关阅读:
    HDU 1010 Tempter of the Bone(DFS剪枝)
    HDU 1013 Digital Roots(九余数定理)
    HDU 2680 Choose the best route(反向建图最短路)
    HDU 1596 find the safest road(最短路)
    HDU 2072 单词数
    HDU 3790 最短路径问题 (dijkstra)
    HDU 1018 Big Number
    HDU 1042 N!
    NYOJ 117 求逆序数 (树状数组)
    20.QT文本文件读写
  • 原文地址:https://www.cnblogs.com/newobjectcc/p/11798410.html
Copyright © 2020-2023  润新知