[DataStructure]线性数据结构之稀疏数组链表栈和队列 Java 代码实现

Posted Spring-_-Bear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[DataStructure]线性数据结构之稀疏数组链表栈和队列 Java 代码实现相关的知识,希望对你有一定的参考价值。

线性数据结构

线性结构指的是数据元素之间存在着“一对一”的线性关系的数据结构。相对应于线性结构,非线性结构的逻辑特征是一个结点元素可能对应多个直接前驱和多个后继。线性数据结构具有以下特征:
1.集合中必存在唯一的一个 “第一个元素”;
2.集合中必存在唯一的一个 “最后的元素”;
3.除最后元素之外,其它数据元素均有唯一的 “后继”;
4.除第一元素之外,其它数据元素均有唯一的 “前驱”

一、稀疏数组

1. 稀疏数组的定义及思想

  1. 稀疏数组的定义:数组中大部分的元素值都未被使用(或都为0),在数组中仅有少部分的空间使用。因此造成内存空间的浪费,为了解决这问题,并且不影响数组中原有的元素值,采用了一种压缩的方式来表示原数组中的数据,这种方式就叫做稀疏数组

  2. 稀疏数组(Sparse Array)的处理方法:稀疏数组中第一部分所记录的是原数组的列数和行数以及有效元素的个数、第二部分所记录的是原数组中有效元素的位置和元素值。把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小了存储的规模

2. 二维数组转换为稀疏数组

   /**
     * 将二维数组转换为稀疏数组
     *
     * @param array 二维数组
     * @return 稀疏数组或 null
     */
   public int[][] arrayToSparse(int[][] array) 
       int rows = array.length;
       int cols = array[0].length;
   
       // 获得原数组中的有效数据个数
       int dataCount = 0;
       for (int[] rowData : array) 
           for (int colData : rowData) 
               if (colData != 0) 
                   dataCount++;
               
           
       
   
       // 创建稀疏数组,由于稀疏数组第一行存储原数组行、列、有效数个数信息,故稀疏数组行数 +1
       int[][] sparseArray = new int[++dataCount][3];
       int sparseCurrentRow = 0;
       // 稀疏数组第一行存储原数组行、列、有效数个数信息
       sparseArray[sparseCurrentRow][0] = rows;
       sparseArray[sparseCurrentRow][1] = cols;
       sparseArray[sparseCurrentRow][2] = --dataCount;
   
       // 遍历原数组,将有效数据的坐标信息及值存入稀疏数组中
       for (int i = 0; i < rows; i++) 
           for (int j = 0; j < cols; j++) 
               int data = array[i][j];
               if (data != 0) 
                   ++sparseCurrentRow;
                   sparseArray[sparseCurrentRow][0] = i;
                   sparseArray[sparseCurrentRow][1] = j;
                   sparseArray[sparseCurrentRow][2] = data;
               
           
       
       return sparseArray;
   

3. 稀疏数组恢复成二维数组

    /**
     * 将稀疏数组恢复成原二维数组
     *
     * @param sparse 稀疏数组
     * @return 二维数组 或 null
     */
   public int[][] sparseToArray(int[][] sparse) 
       if (sparse.length == 0) 
           return null;
       
       // 从稀疏数组的第一行中获取原数组的大小
       int rows = sparse[0][0];
       int cols = sparse[0][1];
   
       int[][] array = new int[rows][cols];
   
       // 从稀疏数组的第一行开始遍历,其结构为 row col val
       for (int i = 1; i < sparse.length; i++) 
           // 给需要恢复的二维数组赋值
           array[sparse[i][0]][sparse[i][1]] = sparse[i][2];
       
   
       return array;
   

二、单链表

1. 单链表的定义

单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据

/**
 * @author Spring-_-Bear
 * @datetime 2022/3/10 15:49
 */
public class SingleLinkedList 
    /**
     * 单链表节点
     */
    public static class ListNode 
        int val;
        public ListNode next;

        public ListNode(int val) 
            this.val = val;
        

        @Override
        public String toString() 
            return "ListNode" +
                    "val=" + val + "";
        
    

2. 尾插法添加节点

    /**
     * 尾插法添加节点(链表带头节点)
     *
     * @param node ListNode
     */
    public void add(ListNode head, ListNode node) 
        ListNode cur = head;
        // 找到链表尾节点
        while (cur.next != null) 
            cur = cur.next;
        
        // 将新节点链接到链表尾
        cur.next = node;
        // 由于新节点 next 域默认初始化为 null,所以省略 node.next = null;
    

3. 尾插法升序添加节点

    /**
     * 尾插法升序添加节点(链表带头节点)
     *
     * @param node ListNode
     */
    public void addInOrder(ListNode head, ListNode node) 
        // 只有头节点,直接添加并返回
        if (head.next == null) 
            head.next = node;
            return;
        

        ListNode current = head;
        // 判断新加入的节点是否要添加到第一个位置,是则直接添加到第一个位置并返回
        if (current.next.val > node.val) 
            node.next = current.next;
            current.next = node;
            return;
        

        current = current.next;
        // 从第一个节点开始遍历整个链表,如果当前节点不指向空且数据比新加入节点的数据小,则当前节点后移,循环寻找新节点的插入位置
        while (current.next != null && current.next.val < node.val) 
            current = current.next;
        

        // 将新节点指向当前节点的下一个节点
        node.next = current.next;
        // 将当前节点指向新节点
        current.next = node;
    

4. 逆序打印链表

方法一:依次从头至尾将链表节点压入栈中,最后依次弹栈输出元素

    /**
     * 逆序打印链表
     *
     * @param head 链表头节点
     */
    public void reversePrint(ListNode head) 
        // 依次将节点放入栈中,再依次出栈
        Stack<ListNode> nodeStack = new Stack<>();
        ListNode cur = head.next;
        while (cur != null) 
            nodeStack.push(cur);
            cur = cur.next;
        

        while (nodeStack.size() > 0) 
            ListNode node = nodeStack.pop();
            System.out.println(node);
        
    

方法二:利用递归逐层返回的特点,使用辅助方法递归遍历到链表尾,在逐层返回的过程中将节点值添加到集合中,最后遍历集合即可实现逆序打印的效果

    private final List<Integer> integerList = new ArrayList<>();

    /**
     * 剑指 Offer 06. 从尾到头打印链表
     *
     * @param head 链表头节点
     * @return 逆序链表值数组
     */
    public int[] reversePrint(ListNode head) 
        /*
         * 递归:利用递归逐层返回的特点,使用辅助方法一直遍历到链表尾,
         * 逐个返回逆序的节点,将节点的值添加到集合中,最后遍历集合元素得到结果数组
         */
        recursion(head);
        int size = integerList.size();
        int[] res = new int[size];
        for (int i = 0; i < size; i++) 
            res[i] = integerList.get(i);
        
        return res;
    

    /**
     * 递归遍历链表
     *
     * @param cur 当前节点
     */
    void recursion(ListNode cur) 
        if (cur == null) 
            return;
        
        recursion(cur.next);
        integerList.add(cur.val);
    

5. 链表反转(不带头节点)

    /**
     * 链表反转(不带头节点)
     *
     * @param head 链表头节点
     */
    public void reverse(ListNode head) 
        // 无节点或只有一个节点,无需反转
        if (head.next == null || head.next.next == null) 
            return;
        
        // 反转链表的头节点
        ListNode reverseHead = new ListNode(-1);
        ListNode cur = head.next;
        ListNode curNext;

        while (cur != null) 
            // 记录当前节点的下一个节点
            curNext = cur.next;
            // 将当前节点指向 reverseHead 的下一个节点即所有插入的新节点都在 reverseHead 与上一次插入的节点之间
            cur.next = reverseHead.next;
            // 将 reverseHead 指向当前节点
            reverseHead.next = cur;
            // 将记录的节点 curNext 赋值给 cur,继续遍历原链表
            cur = curNext;
        
        head.next = reverseHead.next;
    

6. 链表反转(带头节点)

7. 删除节点(带头节点)

    /**
     * 删除单链表中指定的节点
     *
     * @param node ListNode
     */
    public void delete(ListNode head, ListNode node) 
        ListNode current = head;

        // 找到需要删除的节点的前一个节点
        while (current.next != null && !(node.val == current.next.val)) 
            current = current.next;
        

        // 让当前节点指向当前节点的下下个节点
        assert current.next != null;
        current.next = current.next.next;
    

8. 删除节点(不带头节点)

  1. 思路一
    /**
     * 根据 val 删除链表中的对应节点(不带头节点)
     *
     * @param val val
     * @return ListNode
     */
    public ListNode delete(int val) 
        if (head == null) 
            return null;
        

        ListNode temp;
        ListNode current = head;
        // 删除头节点且头节点后无节点
        if (head.val == val && head.next == null) 
            temp = current;
            head = null;
            return temp;
        
        // 删除头节点且头节点后仍有节点
        if (head.val == val) 
            temp = current;
            head = current.next;
            return temp;
        
        // 找到需要删除的节点的前一个节点
        while (current.next != null && current.next.val != val) 
            current = current.next;
        
        // 让当前节点指向当前节点的下下个节点或指向 null
        temp = current.next;
        if (current.next.next != null) 
            current.next = current.next.next;
         else 
            current.next = null;
        
        return temp;
    
  1. 方式二:快慢指针法
    /**
     * 剑指 Offer 18. 删除链表的节点(不带头节点)
     *
     * @param head 头节点
     * @param val  节点值
     * @return 删除指定值节点后的新链表的头节点
     */
    public ListNode deleteNode(ListNode head, int val) 
        if (head == null) 
            return null;
        
        // 要删除的是头节点,直接返回头节点的下一节点
        if (head.val == val) 
            return head.next;
        
        // 双指针法
        ListNode pre = head, cur = head.next;
        // 找到要删除的节点为 cur,pre 为 cur 的前驱节点
        while (cur != null && cur.val != val) 
            pre = cur;
            cur = cur.next;
        
        if (cur != null) 
            pre.next = cur.next;
        
        return head;
    

9. 删除倒数第 k 个节点

    /**
     * 剑指 Offer 22. 链表中倒数第k个节点
     *
     * @param head 头节点
     * @param k    倒数第 k 个节点,从 1 开始计数
     * @return 倒数第 k 个节点后的所有节点
     */
    public ListNode getKthFromEnd(ListNode head, int k) 
        if (head == null || k <= 0) 
            return null;
        

        // 快慢指针法,将慢指针指向当前节点,快指针指向当前节点 + k 个位置
        // 当快指针到达链表尾,直接返回满指针所指节点即为所求
        ListNode fast = head;
        ListNode slow = head;

        // 先将快指针指向第 k 个节点
        while (fast != null && k > 0) 
            fast = fast.next;
            k--;
        

        // 将快指针移到链表尾,返回慢指针
        while (fast != null) 
            fast = fast.next;
            slow = slow.next;
        
        return slow;
    

三、环形链表

1. 环形链表的定义

循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环

2. 约瑟夫问题

据说著名犹太历史学家Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决。Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏

/**
 * @author Spring-_-Bear
 * @datetime 2022/3/10 20:28
 */
public class Joseph 
    /**
     * 第一个节点
     */
    private final PeopleNode first = new PeopleNode(1);
    /**
     * 人数
     */
    private int nums;
    /**
     * 指定报数的数字
     */
    private int digit;
    /**
     * 第一个报数的人的编号
     */
    private int number;

    public static class PeopleNode 
        private final int name;
        public PeopleNode next;

        public PeopleNode(int name) 
            this.name = name;
        

        public int getName() 
            return name;
        

        @Override
        public String toString() 
            return "PeopleNode" +
                    "name='" + name + '\\'' +
                    '';
        
    

    /**
     * 开始游戏
     *
     * @param nums   总人数
     * @param digit  指定要报数的数字
     * @param number 第一个报数的人的编号
     */
    public void startGame(int nums, int digit, int number) 
        if (nums < 1) 
            System.out.println("总人数应大于 1");
            return;
        
        if (digit < 1) 
            System.out.println("指定的报数数字应大于 1");
            return;
        
        if (number < 1) 
            System.out.println("第一个报数的人的编号应大于 1");
            return;
        

        this.nums = nums;
        this.digit = digit;
        this.number = number;
        first.next = first;

        createCircle();
        startCount();
    

    /**
     * 根据总人数创建节点,构建环形链表
     */
    private void createCircle() 
        PeopleNode cur = first;
        for (int i = 2; i <= nums; i++) 
            PeopleNode childNode = new PeopleNode(i);
            childNode.next = first;
            cur.next = childNode;
            cur = cur.next;
        
    

    /**
     * 根据指定的数字开始报数
     */
    private void startCount() 
        PeopleNode cur = first;

        // 先定位到第一个报数的人
        for (int i = 1; i < number; i++) 
            cur = cur.next;
        

        System.out.print("Deque order:");
        while (cur.next != cur) 
            // 获得出队人的编号
            for (int i = 1; i < digit - 1; i++) 
                cur = cur.next;
            
            System.out.print(cur.next.getName() + "\\t");
            // 当前人出队,删除节点
            cur.next = cur.next.next以上是关于[DataStructure]线性数据结构之稀疏数组链表栈和队列 Java 代码实现的主要内容,如果未能解决你的问题,请参考以下文章

稀疏数组与环形数组

[DataStructure]非线性数据结构之哈希表二叉树及多叉树 Java 代码实现

DataStructure

稀疏优化L1范数最小化问题求解之基追踪准则(Basis Pursuit)——原理及其Python实现

稀疏优化L1范数最小化问题求解之基追踪准则(Basis Pursuit)——原理及其Python实现

稀疏数组和队列