• 数据结构与算法(1)稀疏数组、队列、链表


    录:

    1、线性结构和非线性结构
    2、稀疏数组 sparsearray
    3、队列
        3.1、数组模拟队列
        3.2、数组模拟环形队列
    4、链表 Linked List
        4.1、单链表
        4.2、双向链表
        4.3、约瑟夫问题

    1、线性结构和非线性结构    <--返回目录

      数据结构包括:线性结构和非线性结构。

      线性结构:

        1)线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一线性关系。

        2)线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。

        3)链式存储的线性表称为链表,链表中的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。

        4)线性结构常见的有:数组、队列、链表和栈。

      非线性结构:包括二维数组、多维数组、广义表、树结构、图结构。

    2、稀疏数组 sparsearray    <--返回目录

      当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。

      稀疏数组的处理方法是:

      1)记录数组一共有几行几列,有多少个不同的值

      2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模。

       稀疏数组转换的思路

       二维数组 转 稀疏数组的思路

    1. 遍历  原始的二维数组,得到有效数据的个数 sum
    2. 根据sum 就可以创建 稀疏数组 sparseArr   int[sum + 1] [3]
    3. 将二维数组的有效数据数据存入到 稀疏数组
    
    稀疏数组转原始的二维数组的思路
    
    1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的  chessArr2 = int [11][11]
    2. 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.

      二维数组与稀疏数组的转换 Java 代码:

    package com.oy.sparsearray;
    
    /**
     * 稀疏数组
     * 
     * @author oy
     * @date 2020年9月8日 下午10:19:28
     * @version 1.0.0
     */
    public class SparseArray {
        public static void main(String[] args) {
            // 创建一个原始的二维数组 11 * 11
            // 0: 表示没有棋子, 1: 表示 黑子,2: 表示蓝子
            int chessArr1[][] = new int[11][11];
            chessArr1[1][2] = 1;
            chessArr1[2][3] = 2;
            chessArr1[4][5] = 2;
            // 输出原始的二维数组
            System.out.println("原始的二维数组~~");
            for (int[] row : chessArr1) {
                for (int data : row) {
                    System.out.printf("%d	", data);
                }
                System.out.println();
            }
    
            // 将二维数组 转 稀疏数组
            // 1. 先遍历二维数组 得到非0数据的个数
            int sum = 0;
            for (int i = 0; i < 11; i++) {
                for (int j = 0; j < 11; j++) {
                    if (chessArr1[i][j] != 0) {
                        sum++;
                    }
                }
            }
    
            // 2. 创建对应的稀疏数组
            int sparseArr[][] = new int[sum + 1][3];
            // 给稀疏数组赋值
            sparseArr[0][0] = 11;
            sparseArr[0][1] = 11;
            sparseArr[0][2] = sum;
    
            // 遍历二维数组,将非0的值存放到 sparseArr中
            int count = 0; // count 用于记录是第几个非0数据
            for (int i = 0; i < 11; i++) {
                for (int j = 0; j < 11; j++) {
                    if (chessArr1[i][j] != 0) {
                        count++;
                        sparseArr[count][0] = i;
                        sparseArr[count][1] = j;
                        sparseArr[count][2] = chessArr1[i][j];
                    }
                }
            }
    
            // 输出稀疏数组的形式
            System.out.println();
            System.out.println("得到稀疏数组为~~~~");
            for (int i = 0; i < sparseArr.length; i++) {
                System.out.printf("%d	%d	%d	
    ", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
            }
            System.out.println();
    
            // 将稀疏数组 --》 恢复成 原始的二维数组
            /*
             * 1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,比如上面的 chessArr2 = int [11][11] 2.
             * 在读取稀疏数组后几行的数据,并赋给 原始的二维数组 即可.
             */
    
            // 1. 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组
    
            int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
    
            // 2. 在读取稀疏数组后几行的数据(从第二行开始),并赋给 原始的二维数组 即可
    
            for (int i = 1; i < sparseArr.length; i++) {
                chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
            }
    
            // 输出恢复后的二维数组
            System.out.println();
            System.out.println("恢复后的二维数组");
    
            for (int[] row : chessArr2) {
                for (int data : row) {
                    System.out.printf("%d	", data);
                }
                System.out.println();
            }
        }
    }

    3、队列    <--返回目录

      队列是一个有序列表,可以用数组或链表来实现。队列遵循先入先出的原则。即,先存入队列的数据,要先取出。后存入的要后取出。

    3.1、数组模拟队列    <--返回目录

      队列本身是有序列表,若使用数组结构来存储队列的数据,则队列数组的声明如下图,其中maxSize是该队列的最大容量。

      因为队列的输出、输入是分别从前后端来处理,因此需要两个变量front和rear分别记录队列前后端的下标,front会随着数据输出而改变,而rear则是随着输入输入而改变,如下图所示。

      代码:

    package com.oy.queue;
    
    import java.util.Scanner;
    
    public class ArrayQueueDemo {
    
        public static void main(String[] args) {
            //创建一个队列
            ArrayQueue queue = new ArrayQueue(3);
            char key = ' '; //接收用户输入
            Scanner scanner = new Scanner(System.in);//
            boolean loop = true;
            //输出一个菜单
            while(loop) {
                System.out.println("s(show): 显示队列");
                System.out.println("e(exit): 退出程序");
                System.out.println("a(add): 添加数据到队列");
                System.out.println("g(get): 从队列取出数据");
                System.out.println("h(head): 查看队列头的数据");
                key = scanner.next().charAt(0);//接收一个字符
                switch (key) {
                case 's':
                    queue.showQueue();
                    break;
                case 'a':
                    System.out.println("输出一个数");
                    int value = scanner.nextInt();
                    queue.addQueue(value);
                    break;
                case 'g': //取出数据
                    try {
                        int res = queue.getQueue();
                        System.out.printf("取出的数据是%d
    ", res);
                    } catch (Exception e) {
                        // TODO: handle exception
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h': //查看队列头的数据
                    try {
                        int res = queue.headQueue();
                        System.out.printf("队列头的数据是%d
    ", res);
                    } catch (Exception e) {
                        // TODO: handle exception
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'e': //退出
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
                }
            }
            
            System.out.println("程序退出~~");
        }
    
    }
    
    // 使用数组模拟队列-编写一个ArrayQueue类
    class ArrayQueue {
        private int maxSize; // 表示数组的最大容量
        private int front; // 队列头
        private int rear; // 队列尾
        private int[] arr; // 该数据用于存放数据, 模拟队列
    
        // 创建队列的构造器
        public ArrayQueue(int arrMaxSize) {
            maxSize = arrMaxSize;
            arr = new int[maxSize];
            front = -1; // 指向队列头部,分析出front是指向队列头的前一个位置.
            rear = -1; // 指向队列尾,指向队列尾的数据(即就是队列最后一个数据)
        }
    
        // 判断队列是否满
        public boolean isFull() {
            return rear == maxSize - 1;
        }
    
        // 判断队列是否为空
        public boolean isEmpty() {
            return rear == front;
        }
    
        // 添加数据到队列
        public void addQueue(int n) {
            // 判断队列是否满
            if (isFull()) {
                System.out.println("队列满,不能加入数据~");
                return;
            }
            rear++; // 让rear 后移
            arr[rear] = n;
        }
    
        // 获取队列的数据, 出队列
        public int getQueue() {
            // 判断队列是否空
            if (isEmpty()) {
                // 通过抛出异常
                throw new RuntimeException("队列空,不能取数据");
            }
            front++; // front后移
            return arr[front];
        }
    
        // 显示队列的所有数据
        public void showQueue() {
            // 遍历
            if (isEmpty()) {
                System.out.println("队列空的,没有数据~~");
                return;
            }
            for (int i = 0; i < arr.length; i++) {
                System.out.printf("arr[%d]=%d
    ", i, arr[i]);
            }
        }
    
        // 显示队列的头数据, 注意不是取出数据
        public int headQueue() {
            // 判断
            if (isEmpty()) {
                throw new RuntimeException("队列空的,没有数据~~");
            }
            return arr[front + 1];
        }
    }

      问题:

      1)目前数组使用一次就不能用了

      2)将这个数组使用算法,改进成一个环形数组

    3.2、数组模拟环形队列    <--返回目录

      思路:

    思路如下:
    1. front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素 
    front 的初始值 = 0
    2. rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
    rear 的初始值 = 0
    3. 当队列满时,条件是  (rear  + 1) % maxSize == front 【满】
    4. 对队列为空的条件, rear == front 空
    5. 当我们这样分析, 队列中有效的数据的个数   (rear + maxSize - front) % maxSize   // rear = 1 front = 0 
    6. 我们就可以在原来的队列上修改得到,一个环形队列

      代码:

    package com.oy.queue;
    
    import java.util.Scanner;
    
    public class CircleArrayQueueDemo {
    
        public static void main(String[] args) {
            //测试一把
            System.out.println("测试数组模拟环形队列的案例~~~");
            
            // 创建一个环形队列
            CircleArray queue = new CircleArray(4); //说明设置4, 其队列的有效数据最大是3
            char key = ' '; // 接收用户输入
            Scanner scanner = new Scanner(System.in);//
            boolean loop = true;
            // 输出一个菜单
            while (loop) {
                System.out.println("s(show): 显示队列");
                System.out.println("e(exit): 退出程序");
                System.out.println("a(add): 添加数据到队列");
                System.out.println("g(get): 从队列取出数据");
                System.out.println("h(head): 查看队列头的数据");
                key = scanner.next().charAt(0);// 接收一个字符
                switch (key) {
                case 's':
                    queue.showQueue();
                    break;
                case 'a':
                    System.out.println("输出一个数");
                    int value = scanner.nextInt();
                    queue.addQueue(value);
                    break;
                case 'g': // 取出数据
                    try {
                        int res = queue.getQueue();
                        System.out.printf("取出的数据是%d
    ", res);
                    } catch (Exception e) {
                        // TODO: handle exception
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'h': // 查看队列头的数据
                    try {
                        int res = queue.headQueue();
                        System.out.printf("队列头的数据是%d
    ", res);
                    } catch (Exception e) {
                        // TODO: handle exception
                        System.out.println(e.getMessage());
                    }
                    break;
                case 'e': // 退出
                    scanner.close();
                    loop = false;
                    break;
                default:
                    break;
                }
            }
            System.out.println("程序退出~~");
        }
    }
    
    
    class CircleArray {
        private int maxSize; // 表示数组的最大容量
        //front 变量的含义做一个调整: front 就指向队列的第一个元素, 也就是说 arr[front] 就是队列的第一个元素 
        //front 的初始值 = 0
        private int front; 
        //rear 变量的含义做一个调整:rear 指向队列的最后一个元素的后一个位置. 因为希望空出一个空间做为约定.
        //rear 的初始值 = 0
        private int rear; // 队列尾
        private int[] arr; // 该数据用于存放数据, 模拟队列
        
        public CircleArray(int arrMaxSize) {
            maxSize = arrMaxSize;
            arr = new int[maxSize];
        }
        
        // 判断队列是否满
        public boolean isFull() {
            return (rear  + 1) % maxSize == front;
        }
        
        // 判断队列是否为空
        public boolean isEmpty() {
            return rear == front;
        }
        
        // 添加数据到队列
        public void addQueue(int n) {
            // 判断队列是否满
            if (isFull()) {
                System.out.println("队列满,不能加入数据~");
                return;
            }
            //直接将数据加入
            arr[rear] = n;
            //将 rear 后移, 这里必须考虑取模
            rear = (rear + 1) % maxSize;
        }
        
        // 获取队列的数据, 出队列
        public int getQueue() {
            // 判断队列是否空
            if (isEmpty()) {
                // 通过抛出异常
                throw new RuntimeException("队列空,不能取数据");
            }
            // 这里需要分析出 front是指向队列的第一个元素
            // 1. 先把 front 对应的值保留到一个临时变量
            // 2. 将 front 后移, 考虑取模
            // 3. 将临时保存的变量返回
            int value = arr[front];
            front = (front + 1) % maxSize;
            return value;
    
        }
        
        // 显示队列的所有数据
        public void showQueue() {
            // 遍历
            if (isEmpty()) {
                System.out.println("队列空的,没有数据~~");
                return;
            }
            // 思路:从front开始遍历,遍历多少个元素
            // 动脑筋
            for (int i = front; i < front + size() ; i++) {
                System.out.printf("arr[%d]=%d
    ", i % maxSize, arr[i % maxSize]);
            }
        }
        
        // 求出当前队列有效数据的个数
        public int size() {
            // rear = 2
            // front = 1
            // maxSize = 3 
            return (rear + maxSize - front) % maxSize;   
        }
        
        // 显示队列的头数据, 注意不是取出数据
        public int headQueue() {
            // 判断
            if (isEmpty()) {
                throw new RuntimeException("队列空的,没有数据~~");
            }
            return arr[front];
        }
    }

    4、链表 Linked List    <--返回目录

      链表:

        - 链表是有序的列表,以节点的方式存储。

        - 每个节点包含data域、next域,next域指向下一个节点。

        - 链表的各个节点在内存中不一定时连续存储的。

        - 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定。

    4.1、单链表    <--返回目录

      代码

       HeroNode

    package com.oy.linkedlist;
    
    public class HeroNode {
        public int no;
        public String name;
        public String nickname;
        public HeroNode next; // 指向下一个节点
        
        public HeroNode(int no, String name, String nickname) {
            this.no = no;
            this.name = name;
            this.nickname = nickname;
        }
    
        @Override
        public String toString() {
            return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
        }
    }

      SingleLinkedList

    package com.oy.linkedlist;
    
    public class SingleLinkedList {
        // 先初始化一个头节点, 头节点不要动, 不存放具体的数据
        private HeroNode head = new HeroNode(0, "", "");
    
        // 返回头节点
        public HeroNode getHead() {
            return head;
        }
    
        // 添加节点到单向链表
        // 思路,当不考虑编号顺序时
        // 1. 找到当前链表的最后节点
        // 2. 将最后这个节点的next 指向 新的节点
        public void add(HeroNode heroNode) {
            // 因为head节点不能动,因此我们需要一个辅助变量 temp
            HeroNode temp = head;
            // 遍历链表,找到最后
            while (true) {
                // 找到链表的最后
                if (temp.next == null) {
                    break;
                }
                // 如果没有找到最后, 将将temp后移
                temp = temp.next;
            }
            // 当退出while循环时,temp就指向了链表的最后
            // 将最后这个节点的next 指向 新的节点
            temp.next = heroNode;
        }
    
        // 第二种方式在添加英雄时,根据排名将英雄插入到指定位置
        // (如果有这个排名,则添加失败,并给出提示)
        public void addByOrder(HeroNode heroNode) {
            // 因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
            // 因为单链表,因为我们找的temp 是位于 添加位置的前一个节点,否则插入不了
            HeroNode temp = head;
            boolean flag = false; // flag标志添加的编号是否存在,默认为false
            while (true) {
                if (temp.next == null) {// 说明temp已经在链表的最后
                    break;
                }
                if (temp.next.no > heroNode.no) { // 位置找到,就在temp的后面插入
                    break;
                } else if (temp.next.no == heroNode.no) {// 说明希望添加的heroNode的编号已然存在
                    flag = true; // 说明编号存在
                    break;
                }
                temp = temp.next; // 后移,遍历当前链表
            }
            // 判断flag 的值
            if (flag) { // 不能添加,说明编号存在
                System.out.printf("准备插入的英雄的编号 %d 已经存在了, 不能加入
    ", heroNode.no);
            } else {
                // 插入到链表中, temp的后面
                heroNode.next = temp.next;
                temp.next = heroNode;
            }
        }
    
        // 修改节点的信息, 根据no编号来修改,即no编号不能改.
        // 说明
        // 1. 根据 newHeroNode 的 no 来修改即可
        public void update(HeroNode newHeroNode) {
            // 判断是否空
            if (head.next == null) {
                System.out.println("链表为空~");
                return;
            }
            // 找到需要修改的节点, 根据no编号
            // 定义一个辅助变量
            HeroNode temp = head.next;
            boolean flag = false; // 表示是否找到该节点
            while (true) {
                if (temp == null) {
                    break; // 已经遍历完链表
                }
                if (temp.no == newHeroNode.no) {
                    // 找到
                    flag = true;
                    break;
                }
                temp = temp.next;
            }
            // 根据flag 判断是否找到要修改的节点
            if (flag) {
                temp.name = newHeroNode.name;
                temp.nickname = newHeroNode.nickname;
            } else { // 没有找到
                System.out.printf("没有找到 编号 %d 的节点,不能修改
    ", newHeroNode.no);
            }
        }
    
        // 删除节点
        // 思路
        // 1. head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
        // 2. 说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
        public void del(int no) {
            HeroNode temp = head;
            boolean flag = false; // 标志是否找到待删除节点的
            while (true) {
                if (temp.next == null) { // 已经到链表的最后
                    break;
                }
                if (temp.next.no == no) {
                    // 找到的待删除节点的前一个节点temp
                    flag = true;
                    break;
                }
                temp = temp.next; // temp后移,遍历
            }
            // 判断flag
            if (flag) { // 找到
                // 可以删除
                temp.next = temp.next.next;
            } else {
                System.out.printf("要删除的 %d 节点不存在
    ", no);
            }
        }
    
        // 显示链表[遍历]
        public void list() {
            // 判断链表是否为空
            if (head.next == null) {
                System.out.println("链表为空");
                return;
            }
            // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
            HeroNode temp = head.next;
            while (true) {
                // 判断是否到链表最后
                if (temp == null) {
                    break;
                }
                // 输出节点的信息
                System.out.println(temp);
                // 将temp后移, 一定小心
                temp = temp.next;
            }
        }
    }

      SingleLinkedListTest

    package com.oy.linkedlist;
    
    import java.util.Stack;
    
    import org.junit.Test;
    
    public class SingleLinkedListTest {
    
        /**
         * 测试 add() 方法
         */
        @Test
        public void testAdd() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.add(hero1);
            singleLinkedList.add(hero4);
            singleLinkedList.add(hero2);
            singleLinkedList.add(hero3);
    
            // 遍历
            singleLinkedList.list();
        }
    
        /**
         * 测试 addByOrder() 方法
         */
        @Test
        public void testAddByOrder() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            singleLinkedList.list();
        }
    
        /**
         * 测试 update() 方法
         */
        @Test
        public void testUpdate() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            System.out.println("修改前的链表:");
            singleLinkedList.list();
    
            // 修改
            HeroNode newHeroNode = new HeroNode(2, "小卢", "玉麒麟~~");
            singleLinkedList.update(newHeroNode);
    
            System.out.println("修改后的链表:");
            singleLinkedList.list();
        }
    
        /**
         * 测试 del() 方法
         */
        @Test
        public void testDel() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            System.out.println("删除前的链表: ");
            singleLinkedList.list();
    
            singleLinkedList.del(1);
            singleLinkedList.del(4);
            System.out.println("删除后的链表: ");
            singleLinkedList.list();
        }
    
        /**
         * 面试题1:获取到单链表的节点的个数(如果是带头结点的链表,需求不统计头节点)
         * 
         * @param head 链表的头节点
         * @return 返回的就是有效节点的个数
         */
        public static int getLength(HeroNode head) {
            if (head.next == null) { // 空链表
                return 0;
            }
            int length = 0;
            // 定义一个辅助的变量, 这里我们没有统计头节点
            HeroNode cur = head.next;
            while (cur != null) {
                length++;
                cur = cur.next; // 遍历
            }
            return length;
        }
    
        /**
         * 面试题1测试
         */
        @Test
        public void test1() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            singleLinkedList.list();
    
            System.out.println("有效的节点个数=" + getLength(singleLinkedList.getHead()));// 4
    
            // 删除
            singleLinkedList.del(2);
            System.out.println("有效的节点个数=" + getLength(singleLinkedList.getHead()));// 4
        }
    
        /**
         * 面试题2:查找单链表中的倒数第k个结点 【新浪面试题】
         * 1. 编写一个方法,接收head节点,同时接收一个index 
         * 2. index表示是倒数第index个节点 
         * 3. 先把链表从头到尾遍历,得到链表的总的长度 getLength 
         * 4. 得到size 后,我们从链表的第一个开始遍历 (size-index)个,就可以得到 
         * 5. 如果找到了,则返回该节点,否则返回null
         * 
         * @param head
         * @param index
         * @return
         */
        public static HeroNode findLastIndexNode(HeroNode head, int index) {
            // 判断如果链表为空,返回null
            if (head.next == null) {
                return null;// 没有找到
            }
            // 第一个遍历得到链表的长度(节点个数)
            int size = getLength(head);
            // 第二次遍历 size-index 位置,就是我们倒数的第K个节点
            // 先做一个index的校验
            if (index <= 0 || index > size) {
                return null;
            }
            // 定义给辅助变量, for 循环定位到倒数的index
            HeroNode cur = head.next; // 3 // 3 - 1 = 2
            for (int i = 0; i < size - index; i++) {
                cur = cur.next;
            }
            return cur;
        }
        
        /**
         * 面试题2测试
         */
        @Test
        public void test2() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            singleLinkedList.list();
            
            HeroNode res = findLastIndexNode(singleLinkedList.getHead(), 3); 
            System.out.println("res=" + res);
        }
    
        /**
         * 面试题3:单链表反转
         * @param head
         */
        public static void reverse(HeroNode head) {
            // 如果当前链表为空,或者只有一个节点,无需反转,直接返回
            if (head.next == null || head.next.next == null) {
                return;
            }
    
            // 定义一个辅助的指针(变量),帮助我们遍历原来的链表
            HeroNode cur = head.next;
            HeroNode next = null;// 指向当前节点[cur]的下一个节点
            HeroNode reverseHead = new HeroNode(0, "", "");
            // 遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
            // 动脑筋
            while (cur != null) {
                next = cur.next;// 先暂时保存当前节点的下一个节点,因为后面需要使用
                cur.next = reverseHead.next;// 将cur的下一个节点指向新的链表的最前端
                reverseHead.next = cur; // 将cur 连接到新的链表上
                cur = next;// 让cur后移
            }
            // 将head.next 指向 reverseHead.next , 实现单链表的反转
            head.next = reverseHead.next;
        }
        
        /**
         * 面试题3测试
         */
        @Test
        public void test3() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            System.out.println("反转前:");
            singleLinkedList.list();
            reverse(singleLinkedList.getHead());
            System.out.println("反转后:");
            singleLinkedList.list();
        }
        
        // 逆序打印:
        // 可以利用栈这个数据结构,将各个节点压入到栈中,然后利用栈的先进后出的特点,就实现了逆序打印的效果
        public static void reversePrint(HeroNode head) {
            if (head.next == null) {
                return;// 空链表,不能打印
            }
            // 创建要给一个栈,将各个节点压入栈
            Stack<HeroNode> stack = new Stack<HeroNode>();
            HeroNode cur = head.next;
            // 将链表的所有节点压入栈
            while (cur != null) {
                stack.push(cur);
                cur = cur.next; // cur后移,这样就可以压入下一个节点
            }
            // 将栈中的节点进行打印,pop 出栈
            while (stack.size() > 0) {
                System.out.println(stack.pop()); // stack的特点是先进后出
            }
        }
        
        /**
         * 测试逆序打印
         */
        @Test
        public void test4() {
            // 先创建节点
            HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
            HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
            HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
            HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
    
            // 创建单链表
            SingleLinkedList singleLinkedList = new SingleLinkedList();
    
            // 加入
            singleLinkedList.addByOrder(hero1);
            singleLinkedList.addByOrder(hero4);
            singleLinkedList.addByOrder(hero2);
            singleLinkedList.addByOrder(hero3);
    
            // 遍历
            singleLinkedList.list();    
            
            System.out.println("测试逆序打印单链表, 没有改变链表的结构~~");
            reversePrint(singleLinkedList.getHead());
        }
    
    }

    4.2、双向链表    <--返回目录

      HeroNode2

    package com.oy.linkedlist;
    
    public class HeroNode2 {
        public int no;
        public String name;
        public String nickname;
        public HeroNode2 next; // 指向下一个节点, 默认为null
        public HeroNode2 pre; // 指向前一个节点, 默认为null
    
        public HeroNode2(int no, String name, String nickname) {
            this.no = no;
            this.name = name;
            this.nickname = nickname;
        }
    
        @Override
        public String toString() {
            return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
        }
    }

      DoubleLinkedList

    package com.oy.linkedlist;
    
    public class DoubleLinkedList {
        // 先初始化一个头节点, 头节点不要动, 不存放具体的数据
        private HeroNode2 head = new HeroNode2(0, "", "");
    
        // 返回头节点
        public HeroNode2 getHead() {
            return head;
        }
    // 显示链表[遍历]
        public void list() {
            // 判断链表是否为空
            if (head.next == null) {
                System.out.println("链表为空");
                return;
            }
            // 因为头节点,不能动,因此我们需要一个辅助变量来遍历
            HeroNode2 temp = head.next;
            while (true) {
                // 判断是否到链表最后
                if (temp == null) {
                    break;
                }
                // 输出节点的信息
                System.out.println(temp);
                // 将temp后移, 一定小心
                temp = temp.next;
            }
        }
    
        // 添加一个节点到双向链表的最后.
        public void add(HeroNode2 heroNode) {
            // 因为head节点不能动,因此我们需要一个辅助遍历 temp
            HeroNode2 temp = head;
            // 遍历链表,找到最后
            while (true) {
                // 找到链表的最后
                if (temp.next == null) {//
                    break;
                }
                // 如果没有找到最后, 将将temp后移
                temp = temp.next;
            }
            // 当退出while循环时,temp就指向了链表的最后
            // 形成一个双向链表
            temp.next = heroNode;
            heroNode.pre = temp;
        }
    
        // 修改一个节点的内容, 可以看到双向链表的节点内容修改和单向链表一样
        // 只是 节点类型改成 HeroNode2
        public void update(HeroNode2 newHeroNode) {
            // 判断是否空
            if (head.next == null) {
                System.out.println("链表为空~");
                return;
            }
            // 找到需要修改的节点, 根据no编号
            // 定义一个辅助变量
            HeroNode2 temp = head.next;
            boolean flag = false; // 表示是否找到该节点
            while (true) {
                if (temp == null) {
                    break; // 已经遍历完链表
                }
                if (temp.no == newHeroNode.no) {
                    // 找到
                    flag = true;
                    break;
                }
                temp = temp.next;
            }
            // 根据flag 判断是否找到要修改的节点
            if (flag) {
                temp.name = newHeroNode.name;
                temp.nickname = newHeroNode.nickname;
            } else { // 没有找到
                System.out.printf("没有找到 编号 %d 的节点,不能修改
    ", newHeroNode.no);
            }
        }
    
        // 从双向链表中删除一个节点,
        // 说明
        // 1 对于双向链表,我们可以直接找到要删除的这个节点
        // 2 找到后,自我删除即可
        public void del(int no) {
            // 判断当前链表是否为空
            if (head.next == null) {// 空链表
                System.out.println("链表为空,无法删除");
                return;
            }
    
            HeroNode2 temp = head.next; // 辅助变量(指针)
            boolean flag = false; // 标志是否找到待删除节点的
            while (true) {
                if (temp == null) { // 已经到链表的最后
                    break;
                }
                if (temp.no == no) {
                    // 找到的待删除节点的前一个节点temp
                    flag = true;
                    break;
                }
                temp = temp.next; // temp后移,遍历
            }
            // 判断flag
            if (flag) { // 找到
                // 可以删除
                // temp.next = temp.next.next;[单向链表]
                temp.pre.next = temp.next;
                // 这里我们的代码有问题?
                // 如果是最后一个节点,就不需要执行下面这句话,否则出现空指针
                if (temp.next != null) {
                    temp.next.pre = temp.pre;
                }
            } else {
                System.out.printf("要删除的 %d 节点不存在
    ", no);
            }
        }
    
    }

    4.3、约瑟夫问题    <--返回目录

       代码

    public class Josepfu {
    
        public static void main(String[] args) {
            // 测试一把看看构建环形链表,和遍历是否ok
            CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
            circleSingleLinkedList.addBoy(125);// 加入5个小孩节点
            circleSingleLinkedList.showBoy();
            
            //测试一把小孩出圈是否正确
            circleSingleLinkedList.countBoy(10, 20, 125); // 2->4->1->5->3
            //String str = "7*2*2-5+1-5+3-3";
        }
    
    }
    
    // 创建一个环形的单向链表
    class CircleSingleLinkedList {
        // 创建一个first节点,当前没有编号
        private Boy first = null;
    
        // 添加小孩节点,构建成一个环形的链表
        public void addBoy(int nums) {
            // nums 做一个数据校验
            if (nums < 1) {
                System.out.println("nums的值不正确");
                return;
            }
            Boy curBoy = null; // 辅助指针,帮助构建环形链表
            // 使用for来创建我们的环形链表
            for (int i = 1; i <= nums; i++) {
                // 根据编号,创建小孩节点
                Boy boy = new Boy(i);
                // 如果是第一个小孩
                if (i == 1) {
                    first = boy;
                    first.setNext(first); // 构成环
                    curBoy = first; // 让curBoy指向第一个小孩
                } else {
                    curBoy.setNext(boy);//
                    boy.setNext(first);//
                    curBoy = boy;
                }
            }
        }
    
        // 遍历当前的环形链表
        public void showBoy() {
            // 判断链表是否为空
            if (first == null) {
                System.out.println("没有任何小孩~~");
                return;
            }
            // 因为first不能动,因此我们仍然使用一个辅助指针完成遍历
            Boy curBoy = first;
            while (true) {
                System.out.printf("小孩的编号 %d 
    ", curBoy.getNo());
                if (curBoy.getNext() == first) {// 说明已经遍历完毕
                    break;
                }
                curBoy = curBoy.getNext(); // curBoy后移
            }
        }
    
        // 根据用户的输入,计算出小孩出圈的顺序
        /**
         * 
         * @param startNo
         *            表示从第几个小孩开始数数
         * @param countNum
         *            表示数几下
         * @param nums
         *            表示最初有多少小孩在圈中
         */
        public void countBoy(int startNo, int countNum, int nums) {
            // 先对数据进行校验
            if (first == null || startNo < 1 || startNo > nums) {
                System.out.println("参数输入有误, 请重新输入");
                return;
            }
            // 创建要给辅助指针,帮助完成小孩出圈
            Boy helper = first;
            // 需求创建一个辅助指针(变量) helper , 事先应该指向环形链表的最后这个节点
            while (true) {
                if (helper.getNext() == first) { // 说明helper指向最后小孩节点
                    break;
                }
                helper = helper.getNext();
            }
            //小孩报数前,先让 first 和  helper 移动 k - 1次
            for(int j = 0; j < startNo - 1; j++) {
                first = first.getNext();
                helper = helper.getNext();
            }
            //当小孩报数时,让first 和 helper 指针同时 的移动  m  - 1 次, 然后出圈
            //这里是一个循环操作,知道圈中只有一个节点
            while(true) {
                if(helper == first) { //说明圈中只有一个节点
                    break;
                }
                //让 first 和 helper 指针同时 的移动 countNum - 1
                for(int j = 0; j < countNum - 1; j++) {
                    first = first.getNext();
                    helper = helper.getNext();
                }
                //这时first指向的节点,就是要出圈的小孩节点
                System.out.printf("小孩%d出圈
    ", first.getNo());
                //这时将first指向的小孩节点出圈
                first = first.getNext();
                helper.setNext(first); //
                
            }
            System.out.printf("最后留在圈中的小孩编号%d 
    ", first.getNo());
            
        }
    }
    
    // 创建一个Boy类,表示一个节点
    class Boy {
        private int no;// 编号
        private Boy next; // 指向下一个节点,默认null
    
        public Boy(int no) {
            this.no = no;
        }
    
        public int getNo() {
            return no;
        }
    
        public void setNo(int no) {
            this.no = no;
        }
    
        public Boy getNext() {
            return next;
        }
    
        public void setNext(Boy next) {
            this.next = next;
        }
    
    }

    ---

  • 相关阅读:
    使用 requests 维持会话
    使用 requests 发送 POST 请求
    使用 requests 发送 GET 请求
    requests 安装
    使用 urllib 分析 Robots 协议
    使用 urllib 解析 URL 链接
    使用 urllib 处理 HTTP 异常
    使用 urllib 处理 Cookies 信息
    使用 urllib 设置代理服务
    按单生产程序发布
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/13636449.html
Copyright © 2020-2023  润新知