数据结构与算法(四)链表(下)

263 阅读7分钟

单向循环链表

单向循环链表的尾节点会指向首节点形成循环

-w977

单向循环链表的设计

import com.company.AbstractList;

public class SingleCircleLinkedList<E> extends AbstractList<E> {
  private Node<E> first; // 首节点
	
  // 内部类
  private static class Node<E> {
    E element; // 当前节点的元素
    Node<E> next; // 下一个节点
	
    public Node(E element, Node<E> next) {
      this.element = element;
      this.next = next;
    }
  }

  @Override
  public void clear() {
    size = 0;
    first = null; // 首节点为null,后面的所有节点也就都会被释放了
  }

  @Override
  public E get(int index) {
    return node(index).element;
  }

  @Override
  public E set(int index, E element) {
    Node<E> node = node(index);
    E old = node.element;
    node.element = element; // 拿到当前节点元素并覆盖
    return old;
  }

  @Override
  public void add(int index, E element) {
    rangeCheckForAdd(index); // 先判断边界元素是否合格
	
    if (index == 0) { 
      Node<E> newFirst = new Node<>(element, first);
      
      // 拿到最后一个节点(让最后一个节点的next指向首节点)
      // 如果只有一个节点,那么该节点既是首节点,也是尾节点,next指向自己
      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++; // 增加容量
  }

  @Override
  public E remove(int index) {
    rangeCheck(index);
	
    Node<E> node = first;
    
    if (index == 0) { // 如果删除的是首节点
      if (size == 1) { // 如果只有一个元素,那么直接置空
        first = null;
      } else {
        // 让最后一个节点的next指向新的节点
        Node<E> last = node(size - 1);
        first = first.next;
        last.next = first;
      }
    } else { // 如果是其他元素,找到其上一个节点,将其next指向要删除的节点的下一个节点
      Node<E> prev = node(index - 1);
      node = prev.next;
      prev.next = node.next;
    }
    size--; // 减少容量
    return node.element;
  }

  @Override
  public int indexOf(E element) {
    Node<E> node = first;
    if (element == null) {
      for (int i = 0; i < size; i++) {
        if (node.element == null) return i;
	
        node = node.next;
      }
    } else {
      for (int i = 0; i < size; i++) {
        if (element.equals(node.element)) return i;
	
        node = node.next;
      }
    }
    return ELEMENT_NOT_FOUND;
  }
	
  /**
   * 获取index位置对应的节点对象
   * @param index
   * @return
   */
  private Node<E> node(int index) {
    rangeCheck(index); // 首先判断边界元素是否合格
	
	// 从首节点一直循环找到index的位置 
    Node<E> node = first;
    for (int i = 0; i < index; i++) {
      node = node.next;
    }
    return node;
  }
	
  @Override
  public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size=").append(size).append(", [");
    Node<E> node = first;
    
    for (int i = 0; i < size; i++) {
      if (i != 0) {
        string.append(", ");
      }
	
      string.append(node.element);
      node = node.next;
    }
    
    string.append("]");
    
    return string.toString();
  }
}

设计点的详细讲解:

1.添加元素时,要注意首节点的逻辑

如果是空链表添加元素,那么添加的节点既是首节点,也是尾节点,该节点的next指向的也是自己

如果已有元素,那么插入首节点位置需要将尾节点的next指向新的首节点

注意先用一个临时变量保存新的首节点,以防止查找尾节点时的first更改造成查找有误

@Override
public void add(int index, E element) {
  rangeCheckForAdd(index); 
    
  if (index == 0) { 
    Node<E> newFirst = new Node<>(element, 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++; 
}

2.删除元素时,也是需要考虑首节点的问题

如果链表只有一个元素,那么删除该节点就直接让first指向null就可以了

如果链表中不止一个元素,那么删除首节点后需要将尾节点的next指向新的首节点

@Override
public E remove(int index) {
  rangeCheck(index);
    
  Node<E> node = first;
    
  if (index == 0) { // 如果删除的是首节点
    if (size == 1) { // 如果只有一个元素,那么直接置空
      first = null;
    } else {
      // 让最后一个节点的next指向新的节点
      Node<E> last = node(size - 1);
      first = first.next;
      last.next = first;
    }
  } else { // 如果是其他元素,找到其上一个节点,将其next指向要删除的节点的下一个节点
    Node<E> prev = node(index - 1);
    node = prev.next;
    prev.next = node.next;
  }
  size--; // 减少容量
  return node.element;
}

双向循环链表

双向循环链表的首节点的next指向的是尾节点,尾节点的prev指向的是首节点,这样形成循环链表

-w1099

双向循环链表的设计

public class CircleLinkedList<E> extends AbstractList<E> {
  private Node<E> first; // 首节点
  private Node<E> last; // 尾节点
  private Node<E> current; // 指向某个节点

  // 内部类
  private static class Node<E> {
    E element; // 当前节点的元素
    Node<E> prev; // 上一个节点
    Node<E> next; // 下一个节点
    
    public Node(Node<E> prev, E element, Node<E> next) {
      this.prev = prev;
      this.element = element;
      this.next = next;
    }
	
    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder();
	
      if (prev != null) {
        sb.append(prev.element);
      } else {
        sb.append("null");
      }
	
      sb.append("_").append(element).append("_");

      if (next != null) {
        sb.append(next.element);
      } else {
        sb.append("null");
      }
	
      return sb.toString();
    }
  }
  
  // 让current指向头结点
  public void reset() {
    current = first;
  }

  // 让current向后执行一步
  public E next() {
    if (current == null) return null;
	
    current = current.next;
    return current.element;
  }

  // 删除current指向的节点
  public E remove() {
    if (current == null) return null;
	
    Node<E> next = current.next; 
    E element = remove(current);
    if (size == 0) {
      current = null;
    } else {
      current = next;
    }
	
    return element;
  }

  @Override
  public void clear() {
    size = 0;
    first = null;
    last = null;
  }

  @Override
  public E get(int index) {
    return node(index).element;
  }

  @Override
  public E set(int index, E element) {
    Node<E> node = node(index);
    E old = node.element;
    node.element = element; // 拿到当前节点元素并覆盖
    return old;
  }

  @Override
  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、last也都指向自己
			first.next = first; // 自己的prev和next都指向的自己
			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++;
  }

  @Override
  public E remove(int index) {
    rangeCheck(index);
    return remove(node(index));
  }
	
  private 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;
  }
	
  @Override
  public int indexOf(E element) {
    if (element == null) {
      Node<E> node = first;
      for (int i = 0; i < size; i++) {
        if (node.element == null) return i;
	
        node = node.next;
      }
    } else {
      Node<E> node = first;
      for (int i = 0; i < size; i++) {
        if (element.equals(node.element)) return i;
	
        node = node.next;
      }
    }
    return ELEMENT_NOT_FOUND;
  }
	
  /**
   * 获取index位置对应的节点对象
   * @param index
   * @return
   */
  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 (int i = size - 1; i > index; i--) {
        node = node.prev;
      }
      return node;
    }
  }
	
  @Override
  public String toString() {
    StringBuilder string = new StringBuilder();
    string.append("size=").append(size).append(", [");
    Node<E> node = first;
    for (int i = 0; i < size; i++) {
      if (i != 0) {
        string.append(", ");
      }
	
      string.append(node);
	
      node = node.next;
    }
    string.append("]");
    return string.toString();
  }
}

设计点的详细讲解:

1.添加元素时,需要注意边界元素的逻辑

如果添加的是最后一个元素的位置,那么就要将首尾节点的nextprev指向新的节点,并且将last指向新的节点

还要考虑空链表的情况,这时first、last指向的都是新的节点,并且新节点的next、prev也都是指向自己

如果添加的是首节点的位置,也要对应的将first指向新的节点

@Override
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、last也都指向自己
      first.next = first; // 自己的prev和next都指向的自己
      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++;
}

2.删除元素时,同样需要考虑边界元素的逻辑

如果链表中只有一个元素时,那么就直接将first、lastnull就可以了

如果删除的是首尾节点的位置,那么对应的要将上下节点的next、prev做更改,还有first、last更改指向

private 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;
}

3.增加一个成员变量current及对应的函数使循环链表更方便操控

private Node<E> current; // 指向某个节点

// 让current指向头结点
public void reset() {
  current = first;
}

// 让current向后执行一步
public E next() {
  if (current == null) return null;
	
  current = current.next;
  return current.element;
}

// 删除current指向的节点
public E remove() {
  if (current == null) return null;
	
  Node<E> next = current.next; 
  E element = remove(current);
  if (size == 0) {
    current = null;
  } else {
    current = next;
  }
	
  return element;
}

静态链表

前面所学习的链表,是依赖于指针(引用)实现的,有些编程语言是没有指针的,例如早期的BASIC、FORTRAN语言

没有指针的情况下,我们如何实现链表呢?

可以通过数组来模拟链表,称为静态链表

静态链表的实现如下:

  • 数组的每个元素存放两个数据:真实值和下一个元素的索引
  • 数组0位置存放的是头结点信息,如下图所示
  • 最后一个节点元素存放的索引可以是-1,来表示尾节点
  • 如果要是做成循环链表,那尾节点的索引可以存放的是首节点的索引

-w546

其真实顺序如下图所示

-w895

如果数组的每个元素只能存放1个数据要怎么做?

第一种方法,可以考虑用两个数组来做,一个数组存放真实数据,一个数组存放索引,利用数组本身的自己的索引值来相互关联

第二种方法,可以做成二维数组,数组里每个元素又是一个数组,然后元素数组里对应放着两个元素,一个真实数据,一个索引

练习题

1.利用循环链表来实现约瑟夫问题

约瑟夫问题(Josephus Problem)的场景如下

8个人围成一圈,然后从第一个人开始,每数到3的人就要被杀死,这样一直循环,只有最后一个人能存活下来

从1开始,执行杀死的顺序为:3, 6, 1, 5, 2, 8, 4

最后存活的是7

-w1064

这个示例我们刚好可以用双向循环链表来实现

static void josephus() {
  CircleLinkedList<Integer> list = new CircleLinkedList<>();
  for (int i = 1; i <= 8; i++) {
    list.add(i);
  }
	
  // 指向头结点(指向1)
  list.reset();
	
  while (!list.isEmpty()) {
    list.next();
    list.next();
    System.out.println(list.remove());
  }
}

public static void main(String[] args) {
  josephus();	
}

// 执行结果:3, 6, 1, 5, 2, 8, 4,7

实现方案有很多,这里我们就不一一概述了

2.删除链表中的节点

题目概述

-w720

题目链接: leetcode-cn.com/problems/de…

题解:

最终目的只要将第三个节点的值为1换掉就可以了,所以可以将第四个节点的值赋给第三个节点,然后第三个节点的next指向第五个节点就可以了,第四个节点就会被释放掉了

public class ListNode {
  int val;
  ListNode next;
  ListNode(int x) { 
    val = x; 
  }
}

public class Solution {
  public void deleteNode(ListNode node) {
    node.val = node.next.val;
    node.next = node.next.next;
  }
}

3.反转一个单链表

题目概述

-w731

题目链接: leetcode-cn.com/problems/re…

题解(递归方式)

将首节点传进去,通过next会递归调用到最后一个不为null的节点作为新的首节点,而最初的首节点要是想要反转就要通过最初的第二个节点,也就是反转后的最后一位的节点,将其next指向最初的首节点,形成了反转;最后将最初的首节点,也就是反转后的最后一位节点的next指向null就可以了,这样就通过递归形成了反转

public class ListNode {
  int val;
  ListNode next;
  ListNode() {}
  ListNode(int val) { this.val = val; }
  ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

public class Solution {
  public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
	
    ListNode newHead = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return newHead;
  }
}

题解(迭代方式)

先用一个临时变量保存首节点的next,也就是第二个节点,第二个节点指向新的节点,也就是null;再将首节点给了新节点,首节点再去指向临时变量,也就是第二个节点,这样就形成了新的链表;这种方式叫头插法,一直循环进行置换

public class ListNode {
  int val;
  ListNode next;
  ListNode() {}
  ListNode(int val) { this.val = val; }
  ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

public class Solution {
  public ListNode reverseList(ListNode head) {
    if (head == null || head.next == null) return head;
	
    ListNode newHead = null;
    while (head != null) {
      ListNode tmp = head.next;
      head.next = newHead;
      newHead = head;
      head = tmp;
    }
	
    return newHead;
  }
}

4.判断链表中是否有环

题目概述

-w731

题目链接: leetcode-cn.com/problems/li…

题解

是否形成环形链表主要看快慢指针是否会相遇,就好比两个人在操场跑圈,一个跑的快,一个跑的慢,那么最终肯定会相遇。这道题的思路也是如此

class ListNode {
  int val;
  ListNode next;
  ListNode(int x) {
    val = x;
    next = null;
  }
}

public class Solution {	
  public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
	
    ListNode slow = head;
    ListNode fast = head.next;
    while (fast != null && fast.next != null) {
      slow = slow.next;
      fast = fast.next.next;
	
      if (slow == fast) return true;
    }
	
    return false;
  }
}