java数据结构常见面试问题之环形链表问题

173 阅读11分钟
原文链接: www.shsxt.com
通常我们在一些比较大型公司面试中,可能会碰到数据结构的面试,而在面试中出现链表的频率很高,有的比较正常,考链表的常规操作,主要看基本功是否扎实,有些就比较难,难在思维的改变和是否能够想到对应的点。
我们来简单的看看这样的两个问题:
  1. 判断一个单向链表是否是环形链表?
  2. 给定一个未知的长度的单向环形链表,如何确定链表中间位置的节点元素?

1.认识:环形链表

1.1 链表

简单认识链表
何为链表?
链式存储的线性表,简称链表。链表由多个链表元素组成,这些元素称为节点。结点之间通过逻辑连接,形成链式存储结构。存储结点的内存单元,可以是连续的也可以是不连续的。逻辑连接与物理存储次序没有关系。
 
小知识点的拓展:认识下链表的两个重要的域: 指针域与数据域。
指针域用于存放结点的值 
数据域用于存放下一个结点的地址或位置
 
 

1.2  循环链表

循环链表,类似于单链表,也是一种链式存储结构,循环链表由单链表演化过来。单链表的最后一个结点的链域指向NULL,而循环链表的建立,不要专门的头结点,让最后一个结点的链域指向链表结点。 简单点说链表首位相连,组成环状数据结构。如下图结构:



在基于我们在学习JAVASE中学习LinkedList基础之上,我们设计一个环形链表的数据结构。
我们知道在设计环形数据结构的时候,可以得到在设计环形链表需要包含以下几个点:
环形链表设计:
  1. head链表头:记录链表的入口位置
  2. 容量:记录这个元素的个数
  3. Next: 下一个节点元素
根据上述的描述:设计出来的节点伪代码【基于JAVA语言设计】
class Node {
       object value //当前节点值
Node next; // 下一个节点元素
}
备注:因为环形链表是首尾相连,所以最后一个节点的next的存放的头节点的位置
 
 


2. 实现环形链表

首先我们设计一个链表的节点元素的设计
 
根据上述的描述我们节点元素设计模式:
 Node 包含的重要的部分
  1. 数据域: 当前节点值 value 属性
  2. 指针域: 执行下一个节点内存位置
 
Noded的设计模型:


 
 

package com.shsxt.ring;
/**
 * 环形链表节点元素
 */
public class Node {
     
      // 当前节点值
      private T value;
     
      // 下一个节点
      private Node next;
     
      public Node() {}
 
      public Node(T value, Node next) {
           super();
           this.value = value;
           this.next = next;
      }
 
      public T getValue() {
           return value;
      }
 
      public void setValue(T value) {
           this.value = value;
      }
 
      public Node getNext() {
           return next;
      }
 
      public void setNext(Node next) {
           this.next = next;
      }
}
 
 
 
当我们设计好数据结构的节点元素属性后,还需要完成对环形链表的数据结构的设计。
我们命名为 :RingList
 
我们设计环形链表的数据结构时候,包含以下几点:
RingList :
  1. 头指针: head 链表的起始位置。
  2. 容量:   size  链表容量。
 
设计模型:



通过上述的设计模型我们可以看出的环形链表的空间设计模型。
那么接下来我们一起使用JAVA代码是如何实现环形链表。
下图展示了RingList 的属性结构示意图



针对于环型链表的设计,我们可以可以提供以下的两个简单的方式实现。分别是元素添加add() 与元素的获取 get();
 
首先如何实现环形链表的最主要还是在于add () 方法的实现.
 
在我们实现add () 方法我们需要完成以下步骤。
 
我在设计添加元素的采用栈模型的来设计,栈模型就是“先进后出”原则。所以添加元素在头部添加。
  • 创建新节点,设置节点数据域。
  • 判断出节点添加的位置
  •  
  1. 在头部节点没有元素,新节点为第一个元素。
  2. 在头部节点添加新节点,并且将尾部节点的指针指向头部。
  • 容量增加
 

2.1增加方法add

代码设计如下:
public void add(T t) {
       if (t == null) {
           throw new RuntimeException("节点元素为空");
       }
       Node node = new Node();
       node.setValue(t);
       // 如果头节点元素
       if (head == null) {
           head = node;
           last = node;
       } else {
           // 我们按照栈的模式来设计 先进后出的模式
           node.setNext(head); // 将原先的头放置 新元素
           head = node;
           last.setNext(node);// 将最后的节点的指针指向头部
       }
       size++;
    }
 
 

2.2获取元素方法设计

在设计线性链表的获取线性位置,我们借助指针移动的概念设计。
 
首先展示 获取元素方法 get()
public T get(int index) {
 
       if (index < 0) {
           throw new RuntimeException("索引越界 index :" + index);
       }
       if (head != null) {
           Node temp = head;// 遍历的开始位置
           for (int i = 0; i < index; i++) {
              temp = temp.getNext();
           }
           return temp.getValue();//获取节点元素
       }
 
       return null;
    }
 
备注: 根据上述方法的实现,我们 Node temp指向的位置在不断的变化,一直在移动至指定位置。此时这里temp 就是一个指针的概念。
 
 

2.3环形链表完成的设计

 
/**
 * 环形链表
 * @param
 */
public class RingList {
 
    // 头指针
    private Node head;
 
   private int size;
  
   private Node last;
 
   public RingList() {}
 
   public void add(T t) {
       if (t == null) {
           throw new RuntimeException("节点元素为空");
       }
   Node node = new Node();
       node.setValue(t);
       // 如果头节点元素
       if (head == null) {
           head = node;
           last = node;
       } else {
           // 我们按照栈的模式来设计 先进后出的模式
           node.setNext(head); // 将原先的头放置 新元素
           head = node;
           last.setNext(node);// 将最后的节点的指针指向头部
       }
       size++;
   }
 
   /**
    * 取值
    *
    * @param index
    * @return
 */
   public T get(int index) {
 
       if (index < 0) {
           throw new RuntimeException("索引越界 index :" + index);
       }
       if (head != null) {
           Node temp = head;// 遍历的开始位置
           for (int i = 0; i < index; i++) {
               temp = temp.getNext();
           }
           return temp.getValue();//获取节点元素
       }
 
       return null;
   }
 
   public int size() {
       return size;
   }
}
 

3.测试环形链表代码

public class App {
 
  public static void main(String[] args) {
 
      // 创建环形链表
      RingList ring = new RingList<>();
      // 组装数据
      for (int i = 0; i < 20; i++) {
         ring.add("节点"+i);
      }
      System.out.println(ring.size());
      // 获取值
      for (int i = 0; i < 100; i++) {
         System.err.print(ring.get(i) + " , ");
         if (i%10 ==0) {
             System.out.println();
         }
      }
  }
}
 
测试结果展示

 
根据结果我们的得出环形链表的设计成功。
 

4. 应用场景

循环链表的特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
  • 循环链表中没有NULL指针。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等。
 
②在单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。
 
在我们所用的很多优秀的框架 设计数据存储的时候,都是了环形链表的设计。环形链表有多种设计模型。
 
如著名高性能队列 Disruptor, Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级)。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。2011年,企业应用软件专家Martin Fowler专门撰写长文介绍。同年它还获得了Oracle官方的Duke大奖。
 
目前,包括Apache Storm、Camel、Log4j 2在内的很多知名项目都应用了Disruptor以获取高性能
 
后续将会给大家带来Disruptor框架解析。
 
今天我们首先认识环形队列,下周将会给快慢指针的应用。请持续关注。
出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。
 
在我们所用的很多优秀的框架 设计数据存储的时候,都是了环形链表的设计。环形链表有多种设计模型。
 
如著名高性能队列 Disruptor, Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级)。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。2011年,企业应用软件专家Martin Fowler专门撰写长文介绍。同年它还获得了Oracle官方的Duke大奖。
 
目前,包括Apache Storm、Camel、Log4j 2在内的很多知名项目都应用了Disruptor以获取高性能。
 
后续将会给大家带来Disruptor框架解析。
 
今天我们认识了环形队列,接下来大家可以去延伸阅读我另一文章快慢指针的应用。将有大的收获,请多关注。