zoukankan      html  css  js  c++  java
  • 链表问题

    02_链表

    1、链表(Linked List)

    • 动态数组有个明显的缺陷
      • 可能会造成内存空间的大量浪费
    • 能否用到多少就申请多少内存?
      • 链表可以办到这一点
    • 链表是一种链式存储的线性表,所有元素的内存地址不一定是连续的

    Snipaste_2021-03-11_07-11-00

    2、链表的接口设计

    链表的大部分接口和动态数组是一致的

    Snipaste_2021-03-11_07-34-29

    2.1、清空元素 - clear()

    Snipaste_2021-03-11_07-49-00

    清空元素我们只需要将size设置为0,first置为null就好了

    2.2、添加元素 - add(int index,E element)

    比如在1位置前添加一个节点的话,我们只需要将0位置的下一个节点指向新添加的节点,然后将新添加的节点的next指向1节点就实现了元素的添加。

    如图:

    Snipaste_2021-03-11_07-52-05

    添加元素后:

    Snipaste_2021-03-11_07-52-52

    添加完元素之后,我们还要做的就是要进行size++操作

    node方法用于获取index位置的节点

    private Node<E> node(int index){
    	rangeCheck(index);
    	
    	Node<E> node = first;
    	for(int i = 0; i < index; i++){
    		node = node.next;
    	}
    	return node;
    }
    

    添加元素 - 注意0位置

    在添加元素的时候,我们特别的要注意边界位置元素的添加,即我们在0号位置添加元素的时候,我们要让first的next节点指向我们要添加的元素的节点,然后新添加的元素的节点指向原来first节点指向的节点,这样我们就实现了向0号位置添加新的元素的功能

    如图:

    Snipaste_2021-03-11_08-03-50

    public void add(int index,E element){
    	rangeCheck(index);
    	
    	if(index == 0){
    		first = new Node<>(element,first);
    	}else{
    		Node<E> prev = node(index - 1);
    		prev.next = new Node<>(element,prev.next);
    	}
    	
    	size++;
    }
    

    添加元素 - 链表末尾

    向末尾添加元素的时候,我们只需要将原来末尾元素的next指向要添加的新的元素的节点,然后将新的元素的节点指向null就实现了在链表末尾添加元素的功能

    如图:

    Snipaste_2021-03-11_08-06-17

    2.3、删除元素 - remove(int index)

    Snipaste_2021-03-11_08-15-50

    删除元素 - 注意0位置

    删除元素的时候,同样的,我们只需要将要删除的元素的上一个节点指向删除元素下一个节点,就实现了元素的删除。

    public E remove(int index){
    	rangeCheck(index);
    	
    	Node<E> node = first;
    	if(index == 0){
    		first = first.next;
    	}else{
    		Node<E> prev = node(index - 1);
    		node = prev.next;
    		prev.next = node.next;
    	}
    	
    	size--;
    	return node.element;
    }
    

    2.4、反转一个链表

    递归

    Snipaste_2021-03-11_09-27-40

    非递归

    Snipaste_2021-03-11_09-41-30

    ListNode newHead = null;
    while(head != null){
    	ListNode tmp = head.next;
    	head.next = newHead;
    	newHead = head;
    	head = tmp;
    }
    

    2.5、判断链表是否有环

    判断 一个链表是否有环,我们可以用快慢指针的方法,慢指针每次走一步,快指针每次走俩步,如果快指针最后等于慢指针,说明有环,如果快指针最后指向Null,说明没有环

    Snipaste_2021-03-11_09-49-25

    如下图所示,定义一个快指针和一个慢指针即可,步骤如下:

    Snipaste_2021-03-11_09-50-50

    然后慢指针向前走一步,快指针向前走俩步:

    Snipaste_2021-03-11_09-52-15

    重复上面的步骤,最后快慢指针相遇,说明有环的存在

    Snipaste_2021-03-11_09-52-24

    2.7、虚拟头结点

    有时候为了让代码更加精简,统一所有节点的处理逻辑,可以在最前面增加一个虚拟的头结点(不存储数据)

    Snipaste_2021-03-11_10-19-34

    public LinkedList(){
    	first = new Node<>(null,null);
    }
    

    虚拟节点 - node方法

    private Node<E> node(int index){
    	rangeCheck(index);
    	
    	Node<E> node = first.next;
    	for(int i = 0;i < index,i++){
    		node = node.next;
    	}
    	
    	return node;
    }
    

    虚拟节点 - 添加、删除

    public void add(int index, E element){
    	rangeCheckForAdd(index);
    	
    	Node<E> prev = (index == 0) ? first : node(index - 1);
        prev.next = new Node<>(element,prev.next);
        size++;
    }
    
    public E remove(int index){
    	rangeCheck(index);
    	Node<E> prev = (index == 0) ? first : node(index - 1);
    	Node<E> node = prev.next;
    	prev.next = node.next;
    	
    	size--;
    	return node.element;
    }
    

    3、双向链表

    • 此前所学的链表,都是单向链表
    • 使用双向链表可以提升链表的综合性能

    Snipaste_2021-03-11_16-35-52

    3.1、双向链表 - 只有一个元素

    即只有一个元素,next指向Null

    image-20210312151944880

    3.2、双向链表 - node方法

    private Node<E> node(int index){
    	rangeCheck(index);
    	
    	if(index < (size >> 1)){
    		Node<E> node = first;
    		for(int i = 0; i < index; i++){
    			node = node.next;
    		}
    		return node;
    	}else{
    		Node<E> node = last;
    		for(itn i = size - 1; i > index; i--){
    			node = node.prev;
    		}
    		return node;
    	}
    }
    

    3.3、双向链表 - add(int index,E element)

    Snipaste_2021-03-11_16-53-40

    添加元素如下图:

    Snipaste_2021-03-11_16-56-08

    同时我们又要注意像0号位置添加元素,如下图所示:

    Snipaste_2021-03-11_17-09-59

    同样的,我们要不要忽略向链表末尾添加元素

    Snipaste_2021-03-11_17-15-04

    public void add(int index, E element) {
    	rangeCheckForAdd(index);
    
    	// size == 0
    	// index == 0
    	if (index == size) { // 往最后面添加元素
    		Node<E> oldLast = last;
    		last = new Node<>(oldLast, element, null);
    		if (oldLast == null) { // 这是链表添加的第一个元素
    			first = last;
    		} else {
    			oldLast.next = last;
    		}
    	} else { // 正常添加元素
    		Node<E> next = node(index);
    		Node<E> prev = next.prev;
    		Node<E> node = new Node<>(prev, element, next);
    		next.prev = node;
    		if (prev == null) { // index == 0
    			first = node;
    		} else {
    			prev.next = node;
    		}
    	}		
        size++;	
    }
    

    3.4、双向链表 - remove(int index)

    public E remove(int index) {
    	rangeCheck(index);
    
    	Node<E> node = node(index);
    	Node<E> prev = node.prev;
    	Node<E> next = node.next;
    		
    	if (prev == null) { // index == 0
    		first = next;
    	} else {
    		prev.next = next;
    	}
    		
    	if (next == null) { // index == size - 1
    		last = prev;
    	} else {
    		next.prev = prev;
    	}
    	size--;
    	return node.element;
    }
    

    4、单向循环链表

    Snipaste_2021-03-12_11-48-02

    4.1、单向循环链表 - 只有一个节点

    Snipaste_2021-03-12_11-51-49

    4.2、单向循环链表 - add(int index,E element)

    image-20210312152518440

    public void add(int index, E element) {
    	rangeCheckForAdd(index);
    
    	if (index == 0) {
    		Node<E> newFirst = new Node<>(element, first);
    		// 拿到最后一个节点, 上面先不要直接改first, 否则下面找节点会出现问题
    		Node<E> last = (size == 0) ? newFirst : node(size - 1);
    		last.next = newFirst;
    		first = newFirst;
    	} else {
    		Node<E> prev = node(index - 1);
    		prev.next = new Node<>(element, prev.next);
    	}
    	size++;
    }
    

    4.3、单向循环链表 - remove(int index)

    public E remove(int index) {
    	rangeCheck(index);
    
    	Node<E> node = first;
    	if (index == 0) {
    		if (size == 1) {
    			first = null;
    		} else {
    			Node<E> last = node(size - 1);
    			first = first.next;
    			last.next = first;
    		}
    	} else {
    		Node<E> prev = node(index - 1);
    		node = prev.next;
    		prev.next = node.next;
    	}
    	size--;
    	return node.element;
    }
    

    5、双向循环链表

    Snipaste_2021-03-12_12-43-16

    5.1、双向循环链表 - 只有一个节点

    Snipaste_2021-03-12_12-46-50

    5.2、双向循环列表 - add(int index,E element)

    public void add(int index, E element) {
    	rangeCheckForAdd(index);
    
    	if (index == size) { // 往最后面添加元素
    		Node<E> oldLast = last;
    		last = new Node<>(oldLast, element, first);
    		if (oldLast == null) { // 这是链表添加的第一个元素
    			first = last;
    			first.next = first;
    			first.prev = first;
    		} else {
    			oldLast.next = last;
    			first.prev = last;
    		}
    	} else { // 正常添加元素
    		Node<E> next = node(index);
    		Node<E> prev = next.prev;
    		Node<E> node = new Node<>(prev, element, next);
    		next.prev = node;
    		prev.next = node;
    		if (next == first) { // index==0
    			first = node;
    		}
    	}
    	size++;
    }
    

    5.3、双向循环列表 - remove(int index)

    public E remove(Node<E> node) {
    	if (size == 1) {
    		first = null;
    		last = null;
    	} else {
    		Node<E> prev = node.prev;
    		Node<E> next = node.next;
    		prev.next = next;
    		next.prev = prev;
    
    		if (node == first) { // index == 0
    			first = next;
    		}
    
    		if (node == last) { // index == size - 1
    			last = prev;
    		}
    	}
    	size--;		
    	return node.element;	
    }
    

    6、如何发挥循环列表的最大威力?

    • 可以考虑增设一个成员变量,3个方法

      • current : 可以指向某个节点
      • void reset() : 让current指向头结点first
      • E next() : 让current往后走一步,也就是current = current.next
      • E remove() : 删除current指向的节点,删除成功后让current指向下一个节点

      Snipaste_2021-03-12_13-37-06

    7、静态链表

    • 前面所学习的链表,是依赖于指针(引用)实现的
    • 有些编程语言是没有指针的,比如早期的 BASIC、FORTRAN 语言
    • 没有指针的情况下,如何实现链表?
      • 可以通过数组来模拟链表,称为静态链表
      • 数组的每个元素存放 2 个数据:值、下个元素的索引
      • 数组 0 位置存放的是头结点信息
  • 相关阅读:
    How to build Linux system from kernel to UI layer
    Writing USB driver for Android
    Xposed Framework for Android 8.x Oreo is released (in beta)
    Linux Smartphone Operating Systems You Can Install Today
    Librem 5 Leads New Wave of Open Source Mobile Linux Contenders
    GUADEC: porting GNOME to Android
    Librem 5 – A Security and Privacy Focused Phone
    GNOME and KDE Join Librem 5 Linux Smartphone Party
    Purism计划推出安全开源的Linux Librem 5智能手机
    国产系统之殇:你知道的这些系统都是国外的
  • 原文地址:https://www.cnblogs.com/coderD/p/14524286.html
Copyright © 2011-2022 走看看