超硬核讲解数据结构与算法之线性表(二)

简介: 超硬核讲解数据结构与算法之线性表

二、链表


之前我们已经使用顺序存储结构实现了线性表,我们会发现虽然顺序表的查询很快,时间复杂度为O(1),但是增删的效率是比较低的,因为每一次增删操作都伴随着大量的数据元素移动。这个问题有没有解决方案呢?有,我们可以使用另外一种存储结构实现线性表,链式存储结构。


链表是一种物理存储单元上非连续、非顺序的存储结构,其物理结构不能只管的表示数据元素的逻辑顺序,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列的结点(链表中的每一个元素称为结点)组成,结点可以在运行时动态生成。


34fd35d3821b45c4a5c3ed7ec9eb1d39.png

5ff214b0a80b459b835de1dd1c886f2f.png

790f3679464a4e80b7feab228737ffcc.png

那我们如何使用链表呢?按照面向对象的思想,我们可以设计一个类,来描述结点这个事物,用一个属性描述这个结点存储的元素,用来另外一个属性描述这个结点的下一个结点。


结点API设计:


类名 Node
构造方法 Node(T t,Node next):创建Node对象
成员变量 T item:存储数据;
Node next:指向下一个结点


结点类实现:

public class Node<T> {
  //存储元素
  public T item;
  //指向下一个结点
  public Node next;
  public Node(T item, Node next) {
    this.item = item;
    this.next = next;
  }
}

生成链表:

public static void main(String[] args) throws Exception {
  //构建结点
  Node<Integer> first = new Node<Integer>(11, null);
  Node<Integer> second = new Node<Integer>(13, null);
  Node<Integer> third = new Node<Integer>(12, null);
  Node<Integer> fourth = new Node<Integer>(8, null);
  Node<Integer> fifth = new Node<Integer>(9, null);
  //生成链表
  first.next = second;
  second.next = third;
  third.next = fourth;
  fourth.next = fifth;
}

2.1.单向链表


单向链表是链表的一种,它由多个结点组成,每个结点都由一个数据域和一个指针域组成,数据域用来存储数据,指针域用来指向其后继结点。链表的头结点的数据域不存储数据,指针域指向第一个真正存储数据的结点。


2238a62316cf4c9cb48720afc28b6c72.png

2.1.1.单向链表API设计


image.png


2.1.2. 单向链表代码实现

// 单向列表代码
import java.util.Iterator;
public class LinkList<T> implements Iterable<T> {
  //记录头结点
  private Node head;
  //记录链表的长度
  private int N;
  public LinkList(){
    //初始化头结点
    head = new Node(null,null);
    N=0;
  }
  //清空链表
  public void clear(){
    head.next=null;
    head.item=null;
    N=0;
  }
  //获取链表的长度
  public int length(){
    return N;
  }
  //判断链表是否为空
  public boolean isEmpty(){
    return N==0;
  }
  //获取指定位置i出的元素
  public T get(int i){
    if (i<0||i>=N){
      throw new RuntimeException("位置不合法!");
    }
    Node n = head.next;
    for (int index = 0; index < i; index++) {
      n = n.next;
    }
    return n.item;
  }
  //向链表中添加元素t
  public void insert(T t){
    //找到最后一个节点
    Node n = head;
    while(n.next!=null){
      n = n.next;
    }
    Node newNode = new Node(t, null);
    n.next = newNode;
    //链表长度+1
    N++;
  }
  //向指定位置i处,添加元素t
  public void insert(int i,T t){
    if (i<0||i>=N){
      throw new RuntimeException("位置不合法!");
    }
    //寻找位置i之前的结点
    Node pre = head;
    for (int index = 0; index <=i-1; index++) {
      pre = pre.next;
    }
    //位置i的结点
    Node curr = pre.next;
    //构建新的结点,让新结点指向位置i的结点
    Node newNode = new Node(t, curr);
    //让之前的结点指向新结点
    pre.next = newNode;
    //长度+1
    N++;
  }
  //删除指定位置i处的元素,并返回被删除的元素
  public T remove(int i){
    if (i<0 || i>=N){
      throw new RuntimeException("位置不合法");
    }
    //寻找i之前的元素
    Node pre = head;
    for (int index = 0; index <=i-1; index++) {
    pre = pre.next;
  }
  //当前i位置的结点
  Node curr = pre.next;
  //前一个结点指向下一个结点,删除当前结点
  pre.next = curr.next;
  //长度-1
  N--;
  return curr.item;
}
  //查找元素t在链表中第一次出现的位置
  public int indexOf(T t){
    Node n = head;
    for (int i = 0;n.next!=null;i++){
    n = n.next;
    if (n.item.equals(t)){
      return i;
    }
  }
  return -1;
}
  //结点类
  private class Node{
    //存储数据
    T item;
    //下一个结点
    Node next;
    public Node(T item, Node next) {
      this.item = item;
      this.next = next;
    }
  }
  @Override
  public Iterator iterator() {
    return new LIterator();
  }
  private class LIterator implements Iterator<T>{
  private Node n;
  public LIterator() {
    this.n = head;
  }
  @Override
  public boolean hasNext() {
    return n.next!=null;
  }
  @Override
  public T next() {
    n = n.next;
    return n.item;
  }
  }
}
//测试代码
public class Test {
  public static void main(String[] args) throws Exception {
    LinkList<String> list = new LinkList<>();
    list.insert(0,"张三");
    list.insert(1,"李四");
    list.insert(2,"王五");
    list.insert(3,"赵六");
    //测试length方法
    for (String s : list) {
      System.out.println(s);
    }
    System.out.println(list.length());
    System.out.println("-------------------");
    //测试get方法
    System.out.println(list.get(2));
    System.out.println("------------------------");
    //测试remove方法
    String remove = list.remove(1);
    System.out.println(remove);
    System.out.println(list.length());
    System.out.println("----------------");;
    for (String s : list) {
      System.out.println(s);
    }
  }
}


2.2. 双向链表


双向链表也叫双向表,是链表的一种,它由多个结点组成,每个结点都由一个数据域和两个指针域组成,数据域用来存储数据,其中一个指针域用来指向其后继结点,另一个指针域用来指向前驱结点。链表的头结点的数据域不存储数据,指向前驱结点的指针域值为null,指向后继结点的指针域指向第一个真正存储数据的结点。


397a59662b3e4662b5e9d171b0093af2.png

按照面向对象的思想,我们需要设计一个类,来描述结点这个事物。由于结点是属于链表的,所以我们把结点类作为链表类的一个内部类来实现


2.2.1. 结点API设计


image.png


2.2.2.双向链表API设计


image.png


2.2.3.双向链表代码实现

// 双向链表代码
import java.util.Iterator;
public class TowWayLinkList<T> implements Iterable<T>{
  //首结点
  private Node head;
  //最后一个结点
  private Node last;
  //链表的长度
  private int N;
  public TowWayLinkList() {
    last = null;
    head = new Node(null,null,null);
    N=0;
  }
  //清空链表
  public void clear(){
  last=null;
  head.next=last;
  head.pre=null;
  head.item=null;
  N=0;
}
//获取链表长度
public int length(){
  return N;
}
//判断链表是否为空
public boolean isEmpty(){
  return N==0;
}
//插入元素t
public void insert(T t){
  if (last==null){
    last = new Node(t,head,null);
    head.next = last;
  }else{
    Node oldLast = last;
    Node node = new Node(t, oldLast, null);
    oldLast.next = node;
    last = node;
  }
  //长度+1
  N++;
}
//向指定位置i处插入元素t
public void insert(int i,T t){
  if (i<0 || i>=N){
    throw new RuntimeException("位置不合法");
  }
  //找到位置i的前一个结点
  Node pre = head;
  for (int index = 0; index < i; index++) {
    pre = pre.next;
  }
  //当前结点
  Node curr = pre.next;
  //构建新结点
  Node newNode = new Node(t, pre, curr);
  curr.pre= newNode;
  pre.next = newNode;
  //长度+1
  N++;
}
//获取指定位置i处的元素
public T get(int i){
  if (i<0||i>=N){
    throw new RuntimeException("位置不合法");
  }
  //寻找当前结点
  Node curr = head.next;
  for (int index = 0; index <i; index++) {
    curr = curr.next;
  }
  return curr.item;
}
//找到元素t在链表中第一次出现的位置
public int indexOf(T t){
  Node n= head;
  for (int i=0;n.next!=null;i++){
    n = n.next;
    if (n.next.equals(t)){
      return i;
    }
  }
  return -1;
}
//删除位置i处的元素,并返回该元素
public T remove(int i){
  if (i<0 || i>=N){
    throw new RuntimeException("位置不合法");
  }
  //寻找i位置的前一个元素
  Node pre = head;
  for (int index = 0; index <i ; index++) {
    pre = pre.next;
  }
  //i位置的元素
  Node curr = pre.next;
  //i位置的下一个元素
  Node curr_next = curr.next;
  pre.next = curr_next;
  curr_next.pre = pre;
  //长度-1;
  N--;
  return curr.item;
}
//获取第一个元素
public T getFirst(){
  if (isEmpty()){
    return null;
  }
  return head.next.item;
}
//获取最后一个元素
public T getLast(){
  if (isEmpty()){
    return null;
  }
  return last.item;
}
@Override
public Iterator<T> iterator() {
  return new TIterator();
}
private class TIterator implements Iterator{
  private Node n = head;
  @Override
  public boolean hasNext() {
    return n.next!=null;
  }
  @Override
  public Object next() {
    n = n.next;
    return n.item;
  }
}
//结点类
private class Node{
  public Node(T item, Node pre, Node next) {
    this.item = item;
    this.pre = pre;
    this.next = next;
  }
  //存储数据
  public T item;
  //指向上一个结点
  public Node pre;
  //指向下一个结点
  public Node next;
  }
}
//测试代码
public class Test {
  public static void main(String[] args) throws Exception {
    TowWayLinkList<String> list = new TowWayLinkList<>();
    list.insert("乔峰");
    list.insert("虚竹");
    list.insert("段誉");
    list.insert(1,"鸠摩智");
    list.insert(3,"叶二娘");
    for (String str : list) {
      System.out.println(str);
    }
    System.out.println("----------------------");
    String tow = list.get(2);
    System.out.println(tow);
    System.out.println("-------------------------");
    String remove = list.remove(3);
    System.out.println(remove);
    System.out.println(list.length());
    System.out.println("--------------------");
    System.out.println(list.getFirst());
    System.out.println(list.getLast());
  }
}


2.2.4.java中LinkedList实现


java中LinkedList集合也是使用双向链表实现,并提供了增删改查等相关方法


1.底层是否用双向链表实现;

2.结点类是否有三个域


2.3.链表的复杂度分析


get(int i):每一次查询,都需要从链表的头部开始,依次向后查找,随着数据元素N的增多,比较的元素越多,时间复杂度为O(n)


insert(int i,T t):每一次插入,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n);


remove(int i):每一次移除,需要先找到i位置的前一个元素,然后完成插入操作,随着数据元素N的增多,查找的元素越多,时间复杂度为O(n)


相比较顺序表,链表插入和删除的时间复杂度虽然一样,但仍然有很大的优势,因为链表的物理地址是不连续的,它不需要预先指定存储空间大小,或者在存储过程中涉及到扩容等操作,同时它并没有涉及的元素的交换。


相比较顺序表,链表的查询操作性能会比较低。因此,如果我们的程序中查询操作比较多,建议使用顺序表,增删操作比较多,建议使用链表。


2.4.链表反转


单链表的反转,是面试中的一个高频题目。


需求:


原链表中数据为:1->2->3>4


反转后链表中数据为: 4->3->2->1


反转API:


public void reverse() :对整个链表反转
public Node reverse(Node curr) :反转链表中的某个结点curr,并把反转后的curr结点返回


使用递归可以完成反转,递归反转其实就是从原链表的第一个存数据的结点开始,依次递归调用反转每一个结点,直到把最后一个结点反转完毕,整个链表就反转完毕。


dfc3f57ae66f415abf5a5ae7ae86ea25.png

代码 :

public void reverse(){
  if (N==0){
    //当前是空链表,不需要反转
    return;
  }
  reverse(head.next);
}
/**
*
* @param curr 当前遍历的结点
* @return 反转后当前结点上一个结点
*/
public Node reverse(Node curr){
  //已经到了最后一个元素
  if (curr.next==null){
    //反转后,头结点应该指向原链表中的最后一个元素
    head.next=curr;
    return curr;
  }
  //当前结点的上一个结点
  Node pre = reverse(curr.next);
  pre.next = curr;
  //当前结点的下一个结点设为null
  curr.next=null;
  //返回当前结点
  return curr;
}
//测试代码
public class Test {
  public static void main(String[] args) throws Exception {
    LinkList<Integer> list = new LinkList<>();
    list.insert(1);
    list.insert(2);
    list.insert(3);
    list.insert(4);
    for (Integer i : list) {
      System.out.print(i+" ");
    }
    System.out.println();
    System.out.println("--------------------");
    list.reverse();
    for (Integer i : list) {
      System.out.print(i+" ");
    }
  }
}

2.5.快慢指针


快慢指针指的是定义两个指针,这两个指针的移动速度一块一慢,以此来制造出自己想要的差值,这个差值可以然我们找到链表上相应的结点。一般情况下,快指针的移动步长为慢指针的两倍


2.5.1.中间值问题


我们先来看下面一段代码,然后完成需求。

//测试类
public class Test {
  public static void main(String[] args) throws Exception {
    Node<String> first = new Node<String>("aa", null);
    Node<String> second = new Node<String>("bb", null);
    Node<String> third = new Node<String>("cc", null);
    Node<String> fourth = new Node<String>("dd", null);
    Node<String> fifth = new Node<String>("ee", null);
    Node<String> six = new Node<String>("ff", null);
    Node<String> seven = new Node<String>("gg", null);
    //完成结点之间的指向
    first.next = second;
    second.next = third;
    third.next = fourth;
    fourth.next = fifth;
    fifth.next = six;
    six.next = seven;
    //查找中间值
    String mid = getMid(first);
    System.out.println("中间值为:"+mid);
  }
  /**
  * @param first 链表的首结点
  * @return 链表的中间结点的值
  */
  public static String getMid(Node<String> first) {
    return null;
  }
  //结点类
  private static class Node<T> {
    //存储数据
    T item;
    //下一个结点
    Node next;
    public Node(T item, Node next) {
      this.item = item;
      this.next = next;
    }
  }
}

需求:


请完善测试类Test中的getMid方法,可以找出链表的中间元素值并返回。


利用快慢指针,我们把一个链表看成一个跑道,假设a的速度是b的两倍,那么当a跑完全程后,b刚好跑一半,以此来达到找到中间节点的目的。


如下图,最开始,slow与fast指针都指向链表第一个节点,然后slow每次移动一个指针,fast每次移动两个指针


878220e187e2450f8e0e23f524e4df41.png

代码:

/**
* @param first 链表的首结点
* @return 链表的中间结点的值
*/
public static String getMid(Node<String> first) {
  Node<String> slow = first;
  Node<String> fast = first;
  while(fast!=null && fast.next!=null){
    fast=fast.next.next;
    slow=slow.next;
  }
  return slow.item;
}

2.5.2.单向链表是否有环问题


7dce7d60f10043619a767447c89c0148.png

看下面代码,完成需求:

// 测试类
public class Test {
  public static void main(String[] args) throws Exception {
    Node<String> first = new Node<String>("aa", null);
    Node<String> second = new Node<String>("bb", null);
    Node<String> third = new Node<String>("cc", null);
    Node<String> fourth = new Node<String>("dd", null);
    Node<String> fifth = new Node<String>("ee", null);
    Node<String> six = new Node<String>("ff", null);
    Node<String> seven = new Node<String>("gg", null);
    //完成结点之间的指向
    first.next = second;
    second.next = third;
    third.next = fourth;
    fourth.next = fifth;
    fifth.next = six;
    six.next = seven;
    //产生环
    seven.next = third;
    //判断链表是否有环
    boolean circle = isCircle(first);
    System.out.println("first链表中是否有环:"+circle);
  }
  /**
  * 判断链表中是否有环
  * @param first 链表首结点
  * @return ture为有环,false为无环
  */
  public static boolean isCircle(Node<String> first) {
    return false;
  }
  //结点类
  private static class Node<T> {
    //存储数据
    T item;
    //下一个结点
    Node next;
    public Node(T item, Node next) {
      this.item = item;
      this.next = next;
    }
  }
}

需求:


请完善测试类Test中的isCircle方法,返回链表中是否有环。


使用快慢指针的思想,还是把链表比作一条跑道,链表中有环,那么这条跑道就是一条圆环跑道,在一条圆环跑道中,两个人有速度差,那么迟早两个人会相遇,只要相遇那么就说明有环。


473f90b0810c47878986e8c57cbce982.png

b2ab42bf3d1f4aa18d70ddf33ddf4986.png

f8ee499e69b743d0aa05c81ad140ca60.png

2b8cd55461084fffa83c41212ead7b28.pngfe4d5ee8fd1a4cfd9c9dd4fa71a69cff.png

代码:

/**
* 判断链表中是否有环
* @param first 链表首结点
* @return ture为有环,false为无环
*/
public static boolean isCircle(Node<String> first) {
  Node<String> slow = first;
  Node<String> fast = first;
  while(fast!=null && fast.next!=null){
    fast = fast.next.next;
    slow = slow.next;
    if (fast.equals(slow)){
      return true;
    }
  }
  return false;
}


目录
相关文章
|
6月前
|
存储 C语言
数据结构中的线性表链式存储介绍及其基本操作
链式存储是线性表的一种重要存储方式,它通过节点和指针的结构,实现了灵活的动态存储管理。本文介绍了单向链表的基本操作,并提供了相应的C语言代码示例。理解和掌握链表的操作对学习和应用数据结构具有重要意义。希望这篇博客能帮助你更好地理解线性表的链式存储。
137 2
|
2月前
|
存储 Java
数据结构第二篇【关于java线性表(顺序表)的基本操作】
数据结构第二篇【关于java线性表(顺序表)的基本操作】
36 6
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
|
存储
【数据结构】线性表和顺序表
【数据结构】线性表和顺序表
23 1
|
2月前
01(数据结构考研)线性表相关操作代码
01(数据结构考研)线性表相关操作代码
77 0
|
2月前
|
存储 C语言
数据结构之线性表的初始化及其操作
数据结构之线性表的初始化及其操作
42 0
|
3月前
|
存储 Java
java数据结构,线性表顺序存储(数组)的实现
文章介绍了Java中线性表顺序存储(数组)的实现。线性表是数据结构的一种,它使用数组来实现。文章详细描述了线性表的基本操作,如增加、查找、删除、修改元素,以及其他操作如遍历、清空、求长度等。同时,提供了完整的Java代码实现,包括MyList接口和MyLinearList实现类。通过main函数的测试代码,展示了如何使用这些方法操作线性表。
|
6月前
|
存储 测试技术
【数据结构】操作受限的线性表,栈的具体实现
【数据结构】操作受限的线性表,栈的具体实现
66 5
|
6月前
|
存储 测试技术
【数据结构】操作受限的线性表,队列的具体实现
【数据结构】操作受限的线性表,队列的具体实现
50 4
|
6月前
|
存储 算法
数据结构和算法学习记录——特殊线性表之队列-队列的概念、队列结构体类型定义 、基本接口函数、初始化函数、销毁队列函数、入队列函数、判断队列是否为空、出队列函数、读取队头队尾的数据 、计算队列数据个数
数据结构和算法学习记录——特殊线性表之队列-队列的概念、队列结构体类型定义 、基本接口函数、初始化函数、销毁队列函数、入队列函数、判断队列是否为空、出队列函数、读取队头队尾的数据 、计算队列数据个数
42 0