zoukankan      html  css  js  c++  java
  • 数据结构之链表-动图演示

    链表简介

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

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

    链表可以用来实现栈和队列数据结构(栈和队列可理解为逻辑类数据结构,链表属于存储类数据结构),实现缓存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

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

    作者:陆十三
    转载请在明显位置注明出处!
  • 相关阅读:
    Posix线程编程指南(3) 线程同步
    Posix线程编程指南(1) 线程创建与取消
    #pragma once
    pycharm中import动态链接库pyd有错误
    vs2013 + python3.52 + boost1.61, 编译C++库失败
    VS2013下的64位与32位程序配置
    首篇
    B . Medal Ranking -UCF Local Programming Contest 2015
    A . Find the Twins -UCF Local Programming Contest 2015
    Restricted RPS CodeForces
  • 原文地址:https://www.cnblogs.com/newobjectcc/p/11798410.html
Copyright © 2011-2022 走看看