数据结构-单链表

237 阅读6分钟

前言

本章用于主要讲述线性表链式存储中 单链表 的介绍与使用,主要使用单链表实现增删改查

链式存储 - 单链表

介绍:

链式存储是物理存储单元上非连续的、非顺序的存储结构,数据元素的逻辑顺序是通过链表的指针地址实现

规格:

每个元素包含两个结点,一个是存储元素的数据域 (内存空间),另一个是指向下一个结点地址的指针域。根据指针的指向;链表能形成不同的结构,例如 单链表,双向链表,循环链表等。

特点:
  1. 采用动态存储分配,不会造成内存浪费和溢出
  2. 链表在插入删除操作相当方便,只需要修改指针即可,不需要移动大量元素
  3. 由于链表的查找需要从头遍历,越长速度越慢,而且没有下标可用,所有链表的遍历实际上比数组更慢一些
复杂度分析

链表属于常见的一种线性结构,对于插入和移除而言,时间复杂度都为 O(1)

但是对于搜索操作而言,不管从链表的头部还是尾部,都需要遍历 O(n),所以最好复杂度为 O(1) ,最坏的情况就是从头部遍历到尾部才搜索出对应的元素,所以最坏复杂度为 O(n)平均复杂度为 O(n)

 

前奏准备

本文章讲解所用的编程语言为 Java

为了规范化,分为三个java文件
  • Node.java(类):用于定义链表结点
  • NodeLink.java(类):用于实现链表方法
  • Test(类):程序入口

Node.java

定义结点类Node,一个结点有数据域 data 用于存放结点数据与指针域 next 用于指向下一个结点地址

 public class Node {
     Object data; //存储的实际数据
     Node next;// 指向下一个节点的对象
 ​
 ​
     public Node(Object data) {
         this.data = data;
     }
 ​
     public Node(Object data, Node next) {
         this.data = data;
         this.next = next;
     }
 }

NodeLink.java

接下来到重写java接口的方法类,声明原始数组与原始长度,

 public class NodeLink {
     // 定义头结点
     private Node head;
     // 定义长度
     private int len;
 ​
     public NodeLink() {
         this.head = null;
         this.len = 0;
     }
     
     // 方法代码...
 }

利用java内置红线报错,Implement Method 进行自动导入方法;当然你也可以手写( ̄▽ ̄)"

Test.java

在我们定义完结点类与方法类后,就可以开启下面的程序入口了

 public class Test {
     public static void main(String[] args) {
         NodeLink node = new NodeLink();
         // 运行代码...
     }
 }

前奏准备完毕后,我们就可以开始重写方法了 (●'◡'●)

insert(插入)

插入有两种,一种是从头插入作为头结点,一种是指定索引插入结点

从头插入方法的思路:
  1. 进行判断是否符合规范,判断是否从头插入 (健壮性)
  2. 创建新的结点元素
  3. 将新结点的指向旧头结点
  4. 将 head 赋给 新结点
  5. 更新长度
代码实现
 public void insert(int index, Object e) {
         /**
          * index: 插入的索引
          * e: 结点的数据域
          */
         // 判断是否符合规范
         if (index < 0 || index > len) {
             throw new RuntimeException("索引越界");
         }
         // 从头结点插入
         if (index == 0) {
             // 1. 创建新的元素节点
             Node newHead = new Node(e);
             // 2. 将新结点的指向旧头结点
             newHead.next = head;
             // 3. 赋值头结点
             head = newHead;
             
             //*简洁写法,建议将上述理解完后再使用: head = new Node(e, head);
         }
         // 更新长度
         len++;
     }
图文解析

Test中跑一下~

 node.insert(0, "AAA");
 node.insert(0, "BBB");
 node.insert(0, "CCC");

从头插入

指定插入方法的思路:
  1. 进行判断是否符合规范(健壮性)
  2. 创建新的结点元素,获取head为前结点
  3. 循环获取指定下标的前一个结点
  4. 将新结点的next指向前结点的next
  5. 前结点的next 指向新结点
  6. 更新长度
代码示例
  public void insert(int index, Object e) {
         /**
          * index: 插入的索引
          * e: 结点的数据域
          */
         // 1. 判断是否符合规范
         if (index < 0 || index > len) {
             throw new RuntimeException("索引越界");
         }
         // 从头结点插入
         if (index == 0) {
             ...
         }
         // 从指定索引插入结点
         else {
             // 2.创建新的结点元素 
             Node newNode = new Node(e);
             // 获取head为前结点
             Node curNode = head;
             // 3.  获取下标的前一个结点
             for (int i = 0; i < index; i++) {
                 curNode = curNode.next;
             }
             // 4. 将新结点的next指向前结点的next
             newNode.next = curNode.next;
             // 5. 将前结点的next 指向新结点
             curNode.next = newNode;
         }
         // 更新长度
         len++;
     }
图文解析

Test中跑一下~

 node.insert(0, "BBB");
 node.insert(1, "CCC");
 node.insert(1, "AAA");

getNode(获取结点)

用于获取指定结点,多个方法复用;

获取结点方法的思路:
  1. 进行判断是否符合规范(健壮性)
  2. 定义头结点 从头开始找
  3. 循环遍历,因为长度为传入的索引,所以遍历到最后结点就会被找到了
  4. 返回结点
代码示例
     public Object getNode(int index) {
         /**
          * index: 需要获取的结点索引
          */
         if (index < 0 || index >= len) {
             throw new IndexOutOfBoundsException("索引越界");
         }
         // 从头开始找
         Node node = head;
         // 最后一个就是要找的那个结点索引
         for (int i = 0; i < index; i++) {
             node = node.next;
         }
         return node;
     }
图文解析

remove(删除)

删除也有两种情况,从头删除与指定索引删除,不过这里合并一起讲

删除方法的思路:
  1. 进行判断校验(健壮性),判断索引是否超出范围
  2. 从头删除:将头结点直接赋给下一个结点 (逻辑删除)
  3. 指定删除:获取要删除结点的前结点,将要删除的结点指向给前结点的next,将前结点的next 指向删除结点的next
  4. 更新长度
代码示例
     public void remove(int index) {
         /**
          * index: 要删除的结点索引
          */
         if (index < 0 || index >= len) {
             throw new RuntimeException("索引越界");
         }
         if (index == 0) {
             head = head.next;
         } else {
             // 1. 创建要删除的结点的 前结点
             Node preNode = (Node) getNode(index - 1);
             // 2. 将要删除的结点转换为 前结点的下一个结点
             Node delNode = preNode.next;
             // 3. 将前结点的下一个结点转成要删除的结点的下一个结点
             // 相当于直接让要删除的结点失去连接
             preNode.next = delNode.next;
         }
         len--;
     }
图文解析

Test中跑一下~

 node.remove(0); // 从头删除
 node.remove(1); // 指定删除

indexOf(获取索引)

获取指定元素的结点索引

indexof算法的思路:
  1. 定义头结点 从头开始找,定义index 为当前结点索引
  2. 循环判断,如果当前结点的值不为 null 就一直遍历 知道找到了或者遍历结束
  3. 使用equals 与当前结点的数据域与传入的值相匹配 则返回对应索引
  4. 循环查找过程中 头结点 curNode 不停指向下一个,索引随着增加
  5. 默认返回-1
代码示例
     public int indexOf(Object e) {
         /**
          * e: 要查找的元素
          */
         // 从头结点开始查询
         Node curNode = head;
         int index = 0;
         while (curNode != null) {
             // 如果当前结点的数据域与传入的值相匹配 则返回对应索引
             if (curNode.data.equals(e)) {
                 return index;
             }
             // 找不到就换下一个 直到下一个为null
             curNode = curNode.next;
             index++;
         }
         return -1;
     }

get(获取数据) 与 set(修改结点)

由于前提封装方法获取了结点,减少了很多代码量,这两个方法就可以一起写

get与set方法的思路:
  1. 获取指定结点
  2. get 返回指定结点的数据
  3. set 将指定结点的数据修改为传入的数据
代码示例
     public Object get(int index) {
         /**
          * index: 需要获取的结点索引
          * return: 返回结点数据
          */
         Node node = getNode(index);
         return node.data;
     }
 ​
     public void set(int index, Object e) {
         /**
          * index: 需要修改的结点索引
          * e: 需要修改的结点数据
          */
         Node node = getNode(index);
         node.data = e;
     }

length(长度)

获取链表的长度,直接 返回长度

代码示例
     public void length() {
         System.out.println("链表长度:" + this.len);
     }

display(执行)

用于执行打印所有元素的值

indexof方法的思路:
  1. 先拷贝头结点,防止冲突
  2. 循环输出直到结点为空
  3. 每输出一个结点后换下一个结点
代码示例
  public void display() {
         // 展示链表结点
         Node copy_head = head;
         // 1. 循环直到结点为空
         while (copy_head != null) {
             System.out.println( copy_head.data );
             // 2. 打印完后移位下一个
             copy_head = copy_head.next;
         }
     }

打印示例

又到了最后的打印测试,我们来将所有方法测试一下~

以上就是 线性表之链序存储中单链表的定义介绍与方法使用;

ps:链表真是个有趣又好玩的家伙呀 o( ̄▽ ̄)d