单链表及常见算法题

文章目录

    • 一、链表概述
      • 1.1 链表介绍
      • 1.2 代码描述结点
    • 二、单链表的基本操作
      • 2.1 增加结点
      • 2.2 按顺序插入结点
      • 2.3 删除结点
      • 2.4 更新结点
    • 三、单链表笔试题
      • 3.1 查找倒数第 K 个元素
        • 3.1.1 问题描述
        • 3.1.2 问题解决
      • 3.2 反转单链表
        • 3.2.1 问题描述
        • 3.2.2 问题解决
      • 3.3 逆序打印单链表
        • 3.3.1 问题描述
        • 3.3.2 问题解决
      • 3.4 合并两个链表
        • 3.4.1 问题描述
        • 3.4.2 问题解决

一、链表概述

1.1 链表介绍

链表( Linked List ),别名链式存储结构或单链表,是一种常见的基础数据结构,用于存储逻辑关系为 “一对一” 的数据。链表是线性表的一种,但是它并不像顺序表一样是连续存储在内存中的。链表的各个结点散布在各个内存区域,在每一个结点中都存放下一个结点的地址。

单链表在内存中是如下存储的:

单链表及常见算法题_第1张图片

其中:data 域存放的是本结点的数据,next 域存放的是下一个结点的地址

通过上面的图,我们可以得到链表的几个特性:

  1. 链表是以结点的方式来存储的,是链式存储;

  2. 每一个结点包含 data 域、next 域。其中 next 域存放的是下一个结点的地址;

  3. 链表各个结点并不一定是连续(所谓连续指的是地址连续)存储的;

  4. 链表分为带头结点的链表和没有头结点的链表。

1.2 代码描述结点

从上面的定义可以知道,链表中的每个结点中存放的是本结点的数据以及下一个结点的地址。所以链表中的结点可以定义如下:

public ListNode{
     
    public int age;				// 本结点的信息	
    public String name;
    public ListNode next; 		// 下一个结点的地址
}

二、单链表的基本操作

为了便于下面的演示,定义一个结点 HeroNode 如下,该结点可以理解为存储的是一个水浒英雄的信息:

class HeroNode {
     
    private int no;             // 本结点数据
    private String name;
    private String nickName;
    public HeroNode next;       // 指向下一个结点

    public HeroNode(int no, String name, String nickName) {
     
        this.no = no;
        this.name = name;
        this.nickName = nickName;
    }

	// getter and setter and toString ...... 
}

假设我现在有个需求,要求使用链表实现一个水浒英雄榜管理系统。这个系统的具体功能如下:

  1. 可以增加水浒英雄(直接在链表末尾追加)
  2. 可以根据水浒英雄的编号顺序来插入英雄
  3. 可以根据水浒英雄的编号删除英雄
  4. 可以根据编号更新水浒英雄的信息

根据以上的 4 个需求,我们下面将分 4 步来逐一解决。

2.1 增加结点

增加结点,对应的是需求 1,也就是直接在链表的末尾追加一个结点。

思路:

  1. 首先我们需要创建一个头结点,该结点的作用就是表示单链表的头,如果没有头结点,我们是无法知道链表的首个结点是谁、在哪;

  2. 单链表是单向的,所以我们需要从头结点开始遍历整个链表直到末尾,然后增加结点到链表的末尾;

  3. 需要注意的是,头结点是万万不能乱动的,所以我们最好将头结点复制到一个临时结点变量中,对临时变量进行遍历。

代码实现:

// 头结点中是不存储数据的,因此数据无所谓。只要存储下一个结点的地址即可
private static final HeroNode headNode = new HeroNode(0, "", "");

/**
 * @Description 1. 插入结点到链表中(直接在末尾追加)
 */
private static void insertNode(HeroNode node) {
     
    // 首先需要拷贝一份头结点
    HeroNode tempNode = headNode;
    // 插入结点到链表之中,需要首先遍历链表
    while (true) {
     
        // 判断有没有到达链表末尾
        if (tempNode.next == null) {
     
            // 如果当前结点的下一个结点为 null 时,说明当前结点是最后一个结点
            tempNode.next = node;
            node.next = null;
            System.out.println("++++++++++++ 插入结点成功!");
            break;
        }
        // 没有到达链表末尾,那就后移一个位置
        tempNode = tempNode.next;
    }
}

2.2 按顺序插入结点

按顺序插入结点,对应的是需求 2,也就是根据编号从小到大的顺序将英雄插入到链表中。

思路:

  1. 首先还是要创建一个头结点,然后拷贝一个头结点作为辅助变量,使用辅助变量来遍历整个链表;
  2. 如果出现某个结点(假设是 A 结点)的下一个结点(假设是 B 结点)的编号大于待插入结点的情况,那么就首先将 B 结点记录在待插入的结点中,然后再将这个待插入结点插入到 A 结点之后;
  3. 如果遍历到了链表末尾还没找到编号更大的,就直接插入到末尾即可。

代码实现:

/**
 * @Description 2. 根据编号从小到大顺序插入结点
 */
public static void insertNodeByOrder(HeroNode node) {
     
    // 头结点是代码的柱石,不能动,复制一份作为辅助变量
    HeroNode tempNode = headNode;
    // 遍历链表
    while (true) {
     
        if (tempNode.next == null) {
          // 如果到了链表末尾,就直接插入
            tempNode.next = node;
            node.next = null;
            break;
        }
        if (tempNode.next.getNo() > node.getNo()) {
      // 如果当前结点的下一个结点的编号大于要插入的结点,就插入
            // 先让要插入的结点指向下一个结点
            node.next = tempNode.next;
            // 然后再让当前结点指向要插入的结点
            tempNode.next = node;
            break;
        } else if (tempNode.next.getNo() == node.getNo()) {
        // 说明该结点已存在
            System.out.println("!!!!!!!!!! 该结点已存在");
            break;
        }
        // 上述条件都不满足,就继续往后迭代
        tempNode = tempNode.next;
    }
}

2.3 删除结点

删除结点,就是根据编号去查找结点,然后把结点删除掉。

思路:

  1. 首先还是要创建一个头结点,然后拷贝一个头结点作为辅助变量,使用辅助变量来遍历整个链表;
  2. 如果 遍历到某个结点的编号与要查找的给定的编号相同,那么就找到了结点;
  3. 如果遍历结束还没找到,说明该编号不在链表的结点中。

代码实现:

/**
 * @Description 3. 从链表中删除结点
 */
private static void deleteNode(int no) {
     
    // 首先获取到头结点
    HeroNode tempNode = headNode;
    // 遍历结点,找到要删除的结点的前一个结点
    while (true) {
     
        // 判断当前结点是不是最后一个结点,如果到了末尾还没有找到目标结点,报错警告
        if (tempNode.next == null) {
     
            System.out.println("!!!!!!!!!! 目标结点不存在!");
            break;
        }
        if (tempNode.next.getNo() == no) {
     
            // 说明找到了要删除的结点
            // 所谓删除结点,其实就是把要删除的结点的前一个结点的 next 指向要删除的结点的后一个结点
            tempNode.next = tempNode.next.next;
            System.out.println("------------ 更新结点成功!");
            break;
        }
        // 没有找到目标结点,就继续往后移一个位置
        tempNode = tempNode.next;
    }
}

2.4 更新结点

更新结点,对应的是需求 4,也就是根据编号去查找结点,然后更新结点的信息(不包括编号,因为编号是唯一标识)。

思路:

  1. 首先还是要创建一个头结点,然后拷贝一个头结点作为辅助变量,使用辅助变量来遍历整个链表;
  2. 遍历过程中,比对每个结点的编号与要更新的结点的编号是否一致,如果一致则说明找到了要更新的结点。接着将找到的结点中的数据替换成要更新的数据即可;
  3. 如果遍历结束还没找到对应编号的结点,说明链表中不存在这个结点;

代码:

/**
 * @Description 4. 更新结点信息
 */
private static void updateNode(HeroNode node) {
     
    // 首先获取头结点
    HeroNode tempNode = headNode;
    // 遍历结点,找到要修改的结点的前一个结点
    while (true) {
     
        // 判断当前结点是不是最后一个结点,如果到了末尾还没有找到目标结点,报错警告
        if (tempNode.next == null) {
     
            System.out.println("!!!!!!!!!! 目标结点不存在!");
            break;
        }

        if (tempNode.next.getNo() == node.getNo()) {
     
            // 更新结点信息
            tempNode.next.setName(node.getName());
            tempNode.next.setNickName(node.getNickName());
            System.out.println("*********** 更新结点成功!");
            break;
        }
        tempNode = tempNode.next;
    }
}

三、单链表笔试题

3.1 查找倒数第 K 个元素

3.1.1 问题描述

需求:

实现一种算法,找出单向链表中倒数第 k 个结点,返回该结点的值。

示例:

输入: 1->2->3->4->5 和 k = 2
输出: 4

3.1.2 问题解决

思路:

  1. 首先获取单向链表中结点的共个数 size;
  2. 遍历到正数第(size - k + 1)个结点,就是我们要找的倒数第 k 个结点,返回结点值即可。

代码实现:

/**
 * @Description 根据链表的头结点和 k 到链表中查找倒数第 k 个节点
 */
public static int getItem(HeroNode head, int k){
     
    // 1. 首先获取链表总长度
    int size = getSize(head);
    // 2. 链表总长度减去 k 个节点,得到要遍历的节点个数
    int length = size - k;
    // 3. 获取头结点
    HeroNode temp = head;
    // 4. 遍历到倒数第 k 个节点
    for (int i=0; i<length+1; i++){
     
        temp = temp.next;
    }
    return temp.getNo();
}

/**
 * @Description 获取链表中的结点的总个数
 */
public static int getSize(HeroNode head){
     
    // 1. 获取头结点
    HeroNode temp = head;
    int length = 0;
    // 2. 遍历链表,获取节点个数
    while (true){
     
        if (temp.next == null){
          // 如果到了末尾
            break;
        }
        length++;
        temp = temp.next;
    }
    return length;
}

3.2 反转单链表

3.2.1 问题描述

需求:

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

3.2.2 问题解决

思路:

解决这个问题的核心就是头插法

  1. 首先创建一个临时头结点用于记录反转过程中的链表;
  2. 遍历单链表,每遍历到一个有效结点,就让该有效结点指向临时头结点指向的结点;
  3. 临时头结点再指向该有效结点,
  4. 原单链表遍历结束之后,再让原头结点指向临时头结点指向的结点。

代码实现:

/**
* @Description 反转链表
* @Param [head] 头结点
*/
public static void reverseList(HeroNode head){
     
   // 1. 判断链表是否为空或者只有一个结点
   if (head.next == null || head.next.next == null){
     
       return;
   }
   // 2. 定义一个辅助的指针变量,帮助我们遍历原来的链表
   HeroNode cur = head.next;
   HeroNode next = null;   // 指向当前结点[cur] 的下一个结点
   HeroNode reverHead = new HeroNode(0, "", "");	// 用于临时存放反转过程中的链表
   while (cur != null){
     
       next = cur.next;                // 先把当前结点的下一个结点保存下来
       cur.next = reverHead.next;      // 接着让当前结点指向临时反转链表的第一个结点
       reverHead.next = cur;           // 让当前结点作为临时反转链表的第一结点
       cur = next;                     // 移动到原来链表的下一个结点,继续遍历
   }
   head.next = reverHead.next;			// 让原头结点指向反转后的链表
}

3.3 逆序打印单链表

3.3.1 问题描述

需求:

逆序打印一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1

3.3.2 问题解决

思路:

这个问题的解决有多种方案,下面将对各种方案分别进行介绍。

  • 方案一:使用栈。根据栈的先进后出特点,可以先正向遍历将链表中的结点入栈,然后让结点出栈,在出栈的时候打印即可。

  • 方案二:使用递归。首先判断当前结点是否为最后一个结点,如果不是则递归调用,然后在判断语句之外打印每个结点。

    单链表及常见算法题_第2张图片

  • 方案三:直接反转单链表。这个方案就是 3.2 的方案,先将链表反转,然后遍历打印。但是这样会破坏原链表的结构,不推荐使用。

代码实现:

  • 方案一:使用栈

    /**
     * @Description 方案一:使用栈来逆序打印结点
     * @Param [head] 头结点
     */
    public static void printReverse_2(HeroNode head){
           
        Stack<HeroNode> nodeStack = new Stack<>();
        // 去除头结点
        HeroNode cur = head.next;
        // 结点入栈
        while (cur != null){
           
            nodeStack.push(cur);
            cur = cur.next;
        }
        // 结点出栈并打印
        while (nodeStack.size() > 0){
           
            System.out.println(nodeStack.pop());
        }
    }
    
  • 方案二:使用递归

    /**
     * @Description 方案二:使用递归打印结点
     * @Param [node] 链表的第一个结点,即 head.next
     */
    public static void printReverse_3(HeroNode node){
           
        // 这里一定要先递归调用再打印
        if (node.next != null){
           
            printReverse_3(node.next);
        }
        System.out.println(node);
    }
    

3.4 合并两个链表

3.4.1 问题描述

需求:

合并两个有序链表,并且要求合并后的链表依然是一个有序链表。

示例:

输入:1->4->7->NULL
     2->3->5->NULL
输出:1->2->3->4->5->7->NULL

3.4.2 问题解决

思路:

这个问题的核心思想其实就是上面的 2.2 按顺序插入结点到链表中。

  1. 给定两个链表 A、B,假设将链表 A 合并到链表 B 中;
  2. 遍历链表 A 的所有结点,将每个结点按照顺序插入到链表 B 中即可。

代码:

/**
 * @Description 将 head_1 指向的链表按照结点编号从小到大顺序合并到 head_2 指向的链表中
 * @Param [head_1, head_2]
 * @return 合并后的有序链表
 */
public static HeroNode collectList(HeroNode head_1, HeroNode head_2){
     
    HeroNode cur = head_1.next;
    HeroNode next;
    // 遍历第一个链表,将每个结点插入到第二个链表中
    while(cur != null){
     
        next = cur.next;
        insertNode(cur, head_2);
        cur = next;
    }
    return head_2;
}

/**
 * @Description 按照编号从小到大的顺序插入一个结点到 head 指向的链表中
 * @Param [node, head]
 * @return void
 */
public static void insertNode(HeroNode node, HeroNode head){
     
    HeroNode temp = head;
    while (true){
     
        if (temp.next == null){
     
            // 说明遍历到了链表的末尾,直接追加即可
            temp.next = node;
            // 需要注意的是,由于插入的结点可能还记录着其它结点,所以必须要把这个结点记录的地址清空
            // 这样才能顺利记录下一个结点
            node.next = null;
            break;
        }
        if (temp.next.getNo() > node.getNo()){
     
            // 如果该结点小于要插入的链表的结点,那么就把这个结点插入到第一个小于的结点的前一个
            node.next = temp.next;
            temp.next = node;
            break;
        }
        temp = temp.next;
    }
}

你可能感兴趣的:(算法与数据结构,单链表,链表,数据结构,java,算法)