1. 数据结构与算法入门
前言
很早之前做的笔记了,做一个备忘
1.1 经典算法问题:
- 汉诺塔
- 八皇后问题
- 马踏棋盘
1.2 字符串匹配
1.2.1 暴力匹配
1.2.2 KMP算法
1.3 数据结构和算法重要性
- 算法是程序灵魂
- 内存计算框架
1.4 数据结构与算法关系
2. 实际算法问题:
2.1 str.replaceAll( str )
2.1.1 问题:
试写出单链表表示的字符串类以及字符串结点类的定义,并且依次实现它的构造函数,以及计算串的长度,串赋值,判断两串相等,求子串,两串连接,求子串在串中位置等七个成员函数
2.2 其他几个问题:
- 丢手帕问题
- 磁盘问题
- 公交车
- 画图
- 球和篮子
- 扔石头
- 修路问题,最小路径
- 最短路径问题
- 汉诺塔
- 八皇后
2.3 线性结构与非线性结构
- 数据与元素一对一的线性关系
- 顺序存储,元素都是连续的
- 链式存储,元素是不连续的
- 数组,队列,链表和栈
2.3.1 非线性结构
二维数组,多维数组,广义表,树结构,图结构
3. 稀疏数组和队列
3.1 稀疏数组的处理方法:
- 记录数组一共几行几列,有多少个不同的值
- 把具有不同值的元素的行列记录在一个小规模数组
3.1.1 二维数组转稀疏数组的方法
- 遍历原始二维数组,保留有效个数
- 根据sum创建稀疏数组spareArr int[sum+1][3]
- 二维数组的有效数据存入到稀疏数组
3.2 稀疏数组转二维数组:
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组,
- 读取稀疏数组的后几行数据,并赋值给原始的二维数组
4. 稀疏数组的Java代码实现
package array; import java.util.ArrayList; import java.util.List; /** * 稀疏数组 * 1. 需要将二维数组转为稀疏数组存储 * 2. 稀疏数组进行保存(文件读写) * 3. 读取文件恢复稀疏数组(文件读写) * 4. 将稀疏数组转回二维数组 * @author zxd * @title: SpareceArray * @projectName structAlgorithms * @description: 稀疏数组 * @date 2019/8/19 14:54 */ public class SpareceArray { /** * 使用稀疏数组 * @param args 数组 */ public static void main(String[] args) { int[][] twoArrayConvertSparecArray = twoArrayConvertSparecArray(); // 稀疏数组如何转为二维住 SparecArrayConverttwoArray(twoArrayConvertSparecArray); } /** * 稀疏数组转为二维数组 */ private static void SparecArrayConverttwoArray(int[][] array) { // 1. 根据第一行数据还原出二维数组的行与列 int[][] result = new int[array[0][0]][array[0][1]]; for (int i = 1; i < array.length; i++) { result[array[i][0]][array[i][1]] = array[i][2]; } printArray(array); printArray(result); } /** * 稀疏数组转为二维数组的办法 * 1. 创建二维数组,并且加入数据 * 2. */ private static int[][] twoArrayConvertSparecArray() { // 将二维数组转为稀疏数组 初始化为 11 11 int[][] array = new int[8][8]; List list = new ArrayList(); // 在二维数组放两个子 array[4][5] = 2; array[3][7] = 11; array[2][3] = 11; printArray(array); // 计算有几个有效数据 // 存储有几个有效数据 int sumCount = 0; sumCount = calcuArrSize(array, sumCount); // 创建稀疏数组 int[][] spareceArray = new int[sumCount+1][3]; // 第一行为 个数 棋盘的行 棋盘的列 spareceArray[0][0] = array.length; spareceArray[0][1] = array[0].length; spareceArray[0][2] = sumCount; // 稀疏数组存储数据 //count 用于记录是第几个非0数据 int noZeroCount = 0; for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { if(array[i][j] != 0) { noZeroCount++; // 存储第几行 第几列 值为多少 spareceArray[noZeroCount][0] = i; spareceArray[noZeroCount][1] = j; spareceArray[noZeroCount][2] = array[i][j]; } } } return spareceArray; } /** * 计算有效数据的个数 * @param array 原有的数组 * @param sumCount 计算个数 */ private static int calcuArrSize(int[][] array, int sumCount) { for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { if(array[i][j] != 0) { sumCount++; } } } return sumCount; } /** * 打印数组的快捷方法 * @param array 数组 */ private static void printArray(int[][] array) { System.err.println("-----------------我是分割线-----------------"); for (int i = 0; i < array.length; i++) { for (int j = 0; j < array[i].length; j++) { System.out.print(array[i][j] + "\t" ); } System.out.println(); } System.err.println("-----------------我是分割线-----------------"); } }
5. 队列:
5.1 数组模拟队列
5.1.1 思路分析:addQueue
- 将尾指针 rear + 1 , 表示入队。当 rear == front 表示空队列
- 如果rear 等于队列 maxSize - 1, 表示队列满,否则可以增加元素
5.1.2 思路分析:removeQueue
- 将队头指针进行出队操作
- 每次移除都需要队头指针 +1
- 将队列后面的数据向前拷贝
5.2 JAVA代码实现数组底层的队列
package array; import javax.sql.rowset.serial.SerialArray; import java.util.Arrays; /** * 使用数组实现队列 * * @author zhaoxudong * @title: MyQueue * @projectName structAlgorithms * @description: 使用数组实现队列 * @date 2019/8/23 15:27 */ public class MyQueue { /** * 队尾指针 */ private int rear; /** * 队头指针 */ private int front; /** * 最大数 */ private int maxSize; /** * 队列 */ private int[] arr; public MyQueue(int size) { if (size <= 0) { System.err.println("队列不能小于或者等于 0"); return; } // 注意此处为什么是-1 :因为队列的头部不应该执行队伍的第一个元素,因为此时是出于队列的头部,并没有数据 rear = front = -1; this.arr = new int[size]; this.maxSize = size; } /** * 出队操作: * 1. 队伍头部的元素删除即,使 return arr[front] * 2. 将队伍后面的元素向前拷贝 */ public int removeQueue() { if (isEmpty()) { System.err.println("队列为空,不能移除"); return -1; } for (int i = 1; i < rear; i++) { arr[i - 1] = arr[i]; } // arr[--rear] = 0; return arr[++front]; } /** * 队尾+1 入队操作 * 需要判断队列是否为空并且是否不满 * * @param val */ public void addQueue(int val) { // 判断是否为空或者是否已经满了 boolean empty = isFull(); if (!empty) { System.err.println("队列已满,无法添加"); return; } arr[++rear] = val; } /** * 判断队列是否可以加入数据 * * @return */ private boolean isFull() { return rear != maxSize - 1; } private boolean isEmpty() { return rear == front; } public void showQueue(){ System.err.println(Arrays.toString(this.arr)); } public static void main(String[] args) { MyQueue myQueue = new MyQueue(3); myQueue.addQueue(2); myQueue.addQueue(5); myQueue.addQueue(6); myQueue.removeQueue(); myQueue.removeQueue(); myQueue.removeQueue(); // 这里出现问题,因为队头和队尾都指向队列的尾部,会出现无法添加和无法删除掉问题 myQueue.addQueue(6); myQueue.removeQueue(); myQueue.showQueue(); } }
5.2.1 数组队列的弊端
- 队头和队尾都执行队列的尾部时候,无法添加也无法删除
- 数组只能使用一次
- 数组复用性不好
5.3 数组实现循环队列
5.3.1 如何判定队列已经满了
算法
( rear + 1 ) % maxSize == front
5.3.2 如何判定队列是空的
rear == front
5.3.3 如何判定有效个数
举例:
maxSize = 7
front = 5
rear = 5
maxSize + (rear+1) - front % maxSize
5.3.4 使用数组实现循环队列的方式2
package array; /** * 使用数组实现队列 * * @author zhaoxudong * @title: MyQueue * @projectName structAlgorithms * @description: 使用数组实现队列 * @date 2019/8/23 15:27 */ public class MyQueue { /** * 队尾指针 */ private int rear; /** * 队头指针 */ private int front; /** * 最大数 */ private int maxSize; /** * 队列 */ private int[] arr; public MyQueue(int size) { if (size <= 0) { System.err.println("队列不能小于或者等于 0"); return; } // 注意此处为什么是-1 :因为队列的头部不应该执行队伍的第一个元素,因为此时是出于队列的头部,并没有数据 rear = front = 0; this.arr = new int[size]; this.maxSize = size; } /** * 出队操作: * 1. 队伍头部的元素删除即,使 return arr[front] * 2. 将队伍后面的元素向前拷贝 */ public int removeQueue() { if (isEmpty()) { System.err.println("队列为空,不能移除"); return -1; } // for (int i = 1; i < rear; i++) { // arr[i - 1] = arr[i]; // } // arr[--rear] = 0; return arr[++front]; } /** * 队尾+1 入队操作 * 需要判断队列是否为空并且是否不满 * * @param val */ public void addQueue(int val) { // 判断是否为空或者是否已经满了 boolean empty = isFull(); if (empty) { System.err.println("队列已满,无法添加"); return; } arr[rear] = val; rear = (rear + 1) % maxSize; } /** * 判断队列是否可以加入数据 * * @return */ private boolean isFull() { // return rear != maxSize - 1; // 这是最新的一种判定方式: rear = return (rear + 1) % maxSize == front; } private boolean isEmpty() { return rear == front; } public void showQueue(){ if(arr == null || arr.length == 0){ System.err.println("队列为空,不能遍历"); return; } for (int i = front; i < front + size(); i++) { System.err.print(arr[i % maxSize] + "\t"); } // System.err.println(Arrays.toString(this.arr)); } public int size(){ return (rear + maxSize - front) % maxSize; } public static void main(String[] args) { MyQueue myQueue = new MyQueue(8); myQueue.addQueue(2); myQueue.addQueue(55); myQueue.addQueue(35); myQueue.addQueue(25); myQueue.addQueue(65); myQueue.addQueue(85); myQueue.addQueue(75); // myQueue.addQueue(6); myQueue.removeQueue(); myQueue.removeQueue(); myQueue.removeQueue(); // 这里出现问题,因为队头和队尾都指向队列的尾部,会出现无法添加和无法删除掉问题 // myQueue.addQueue(6); // myQueue.removeQueue(); myQueue.showQueue(); } }
6. 链表
6.1 使用自制的链表处理
6.1.1 使用java代码实现链表
package queue; /** * 单链表实现 * * @author zhaoxudong * @title: SpareceArray * @projectName structAlgorithms * @description: 单链表实现 * @date 2019/8/19 14:54 */ public class SingelQueue { public static void main(String[] args) { HeroNode heroNode1 = new HeroNode(0, "111", "111"); HeroNode heroNode2 = new HeroNode(0, "222", "222"); HeroNode heroNode3 = new HeroNode(0, "333", "333"); HeroNode heroNode4 = new HeroNode(0, "444", "444"); SingelLinkedList singelLinkedList = new SingelLinkedList(); singelLinkedList.add(heroNode1); singelLinkedList.add(heroNode2); singelLinkedList.add(heroNode3); singelLinkedList.add(heroNode4); singelLinkedList.list(); } } class SingelLinkedList { private HeroNode head = new HeroNode(0, "", ""); /** * 在插入的使用最新的方式: * 根据id编号进行排序 * 1. * @param node */ public void add(HeroNode node) { // 1. 由于头结点不能动,需要使用 HeroNode heroNode = head; while (true) { if(heroNode.getNext() == null){ break; } heroNode = heroNode.getNext(); } heroNode.setNext(node); } public void list(){ HeroNode heroNode = head; while (true) { if(heroNode.getNext() == null){ break; } System.err.println(heroNode); heroNode = heroNode.getNext(); } } } /** * 构造函数的节点 */ class HeroNode { private int no; private String name; private String nickName; private HeroNode next; /** * 初始化构造函数 * * @param no * @param name * @param nickName */ public HeroNode(int no, String name, String nickName) { this.no = no; this.name = name; this.nickName = nickName; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNickName() { return nickName; } public void setNickName(String nickName) { this.nickName = nickName; } public HeroNode getNext() { return next; } public void setNext(HeroNode next) { this.next = next; } @Override public String toString() { return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickName='" + nickName + '\'' + ", next=" + next + '}'; } }
6.1.2 使用排序的方式插入链表改进上述链表
- 这里使用了查找的方式,判断是否相等,遍历的时候使用头指针的下一个节点进行遍历操作
- 插入节点使用被插入的节点的下一个节点指向当前遍历节点的下一个节点,当前循环节点指向指向当前被插入的节点
- 如果节点相等,给出提示
/** * 在插入的使用最新的方式: * 使用id编号进行排序,在查找的时候使用id进行排序查找 * @param node */ public void addByOrder(HeroNode node) { // 1. 由于头结点不能动,需要使用 HeroNode heroNode = head; boolean flag = false; while (true) { if(heroNode.getNext() == null){ break; } // 寻找编号大于被插入节点的节点,因为只能找被插入的节点的上一个节点 if(heroNode.getNext().getNo() > node.getNo()){ // 获取原来数据的下一个节点一个节点 break; }else if(heroNode.getNext().getNo() == node.getNo()){ //说明希望添加的heroNode的编号已然存在 flag = true; break; } heroNode = heroNode.getNext(); } if(flag) { System.err.println("被插入的节点已经存在"); }else{ node.setNext(heroNode.getNext()); heroNode.setNext(node); } }
6.1.3 删除节点有多少种情况
- 如果删除的是头节点
- 如果是中间的节点,需要将被删除的上一个节点执行被删除节点的下一个节点
- 如果在尾部,删除尾部节点
6.1.4 删除的第一种方式实现办法
/** * 删除方式, * 第一种方式: 使用编号删除对应的节点 * * @param no 节点编号 */ public void del(int no) { HeroNode temp = head; // 如果是头节点指向节点,需要变更头节点指向节点再删除节点 if (temp.getNext().getNo() == no) { // 指向下一个节点的下一个节点 head.setNext(head.getNext().getNext()); return; } // 判断是否找到了节点 boolean flag = false; while (true) { if (temp.getNext().getNo() == no) { flag = true; break; } temp = temp.getNext(); } // 找到节点进行替换 if(flag){ temp.setNext(temp.getNext().getNext()); }else{ System.err.println("没有找到对应编号的英雄"); } }
6.1.5 删除的第二种方式分析:
- 删除根据第几个来删除,必须我想删除第一个节点
- 第一个节点对应了头节点,需要对于头指针指向进行后移
- 其余根据no编号删除雷同,只需要编号判断改为第几个判断即可
6.2 关于链表的面试题(重点)
- 单链表当中有效节点的个数
- 获取倒数第n个节点的节点
- 反转链表
- 定义一个节点:反转用的头
- 从头到尾遍历原来链表,每遍历一个节点,就取出,放到新的链表前面
- 将旧链表的下一个节点指向逆序链表的下一个节点
- 从尾部到头部逆序打印链表
- 第一种方法:显逆序,逆序之后打印节点(不可取)
- 使用栈实现:将所有的节点加入栈当中,然后使用栈进行打印
6.2.1 节点个数的实现
较为简单,遍历统计遍历几次即可
6.2.2 倒数第N个节点
/** * 获取倒数第index个节点的值 * @param index * @return */ public HeroNode getLastIndexHeroNode(int index){ // 1. 获取链表的总个数 int count = count(); if(index <= 0 || index > count){ return null; } HeroNode temp = head.getNext(); // 2. 根据size - index , 循环获取倒数第 index 节点 int i = 0; int size = count-index; while (true) { if (i == size) { break; } i++; temp = temp.getNext(); } // 3. 遍历拿到数据,返回结果 return temp; }
6.2.3 反转链表
/** * 反转节点 * 1. 定义一个反转节点的head * 2. 遍历原来的链表,使用类似入栈的方式,将每个节点接入到反转节点的指向节点,被接入节点指向原来的节点 * 3. 反转节点完成 */ public void reverse(){ if(head == null || head.getNext() == null || head.getNext().getNext() == null){ System.err.println("无需反转"); return; } HeroNode reverseHead = new HeroNode(0, "", ""); HeroNode temp = head.getNext(); HeroNode next = null; while (temp != null) { // 让当前节点接到新链表的下一个节点 next = temp.getNext(); temp.setNext(reverseHead.getNext()); reverseHead.setNext(temp); temp = next; } // 使用反转节点替换原来的节点 head.setNext(reverseHead.getNext()); }
6.2.4 反序打印链表
- 需要使用到栈这种结构,将节点压进栈当中,然后从栈中取数据进行遍历
/** * 反序打印链表里面的内容 * 使用的栈结构进行存放数据,然后使用栈进行输出操作 */ public void reversePrint() { System.err.println(); if (head == null || head.getNext() == null || head.getNext().getNext() == null) { System.err.println("无需反转打印"); return; } HeroNode hero = head.getNext(); Stack<HeroNode> stack = new Stack<>(); while (hero != null) { stack.push(hero); hero = hero.getNext(); } while(stack.size()>0) { System.out.println(stack.pop()); } }
6.2.5 合并两个链表
- 遍历两个链表,先将第一个链表进行插入,然后根据第二个链表进行相同的插入操作s
/** * 合并两个链表,合并之后依然有序 * 1. 计算两个链表的个数 * 2. 循环总数,判断两个节点的当前节点更大, * * @param list1 * @param list2 * @return */ public static SingelLinkedList mergeList(SingelLinkedList list1, SingelLinkedList list2) { if(list1 == null && list2 == null){ return null; } if(list1 == null) { return list2; } if(list2 == null) { return list1; } SingelLinkedList result = new SingelLinkedList(); HeroNode next1 = list1.getHead(); while (true) { if (next1 == null) { break; } HeroNode next = next1.getNext(); SingelLinkedList.addByOrder(result, next1); next1 = next; } HeroNode next2 = list2.getHead().getNext(); while(true){ if(next2 == null) { break; } HeroNode next = next2.getNext(); SingelLinkedList.addByOrder(result, next2); next2 = next; } return result; }
6.3 分析双向链表的遍历,添加、删除的操作思路
- 遍历方和单链表一样只是可以朝前,可以向后
- 添加
- 默认添加到双向链表的最后
- 先找到双向链表的最后这个节点
- temp.next = new heronode
- newheronode.pre = temp
- 删除
- 因为是双向的,可以自我删除
- 找到要删除的这个节点,temp
- temp.next.pre = temp.pre
- temp.pre.next = temp.next
- 修改
- 不需要太大变动,和原来的链表类似
6.4 循环链表(约瑟夫问题)
6.4.1 什么是循环链表
无论是加入还是删除节点,最后一个节点要么指向自己,要么指向链表的头节点,形成环状的一个链表
6.4.2 构建一个环形链表的思路
- 先创建一个节点,first 指向改节点, 并且形成环形
- 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表当中即可
/** * 环形链表的添加和原来的添加代码有所不同 * @param size */ public void add(int size){ if(size < 1){ System.out.println("size的值不正确"); return; } CircularNode cur = null; for (int i = 1; i <= size; i++) { CircularNode newcur = new CircularNode(i, "小孩"+i, "小孩昵称"+i); // 如果是第一个节点,则需要特殊处理 if(i == 1){ // 第一个节点复制给first节点 first = newcur; first.setNext(first); // 将当前操作指针指向第一个节点 cur = first; }else{ cur.setNext(newcur); newcur.setNext(first); cur = newcur; } } }
6.4.3 遍历环形链表
- 先让一个辅助指针 curBoy , 指向first
- 通过while循环,遍历环形链表,当curBoy.next == first 结束
/** * 遍历节点 */ public void list(){ if(first == null){ System.err.println("循环链表为空"); return; } CircularNode circularQueue = first; while(true){ System.err.println(circularQueue); if(circularQueue.getNext() == first){ break; } circularQueue = circularQueue.getNext(); } }
6.4.4 模拟约瑟夫问题的实现(重点)
/** * * @param begin 从第几个小孩开始数 * @param countNum 数几个小孩 * @param nums 表示最初有多少小孩在圈中 */ public void count(int begin, int countNum, int nums){ // 先对数据进行校验 if (first == null || begin < 1 || countNum > nums) { System.out.println("参数输入有误, 请重新输入"); return; } // 创建辅助指针,指向环形链表的最后一个元素 CircularNode helper = first; while(helper.getNext() != first){ helper = helper.getNext(); } //小孩报数前,先让 first 和 helper 移动 k - 1次 for (int i = 0; i < begin; i++) { first = first.getNext(); helper = helper.getNext(); } // 当小孩报数时,让first 和 helper 指针同时 的移动 m - 1 次, 然后出圈 // 这里是一个循环操作,知道圈中只有一个节点 while(true){ if(helper == first) { break; } //让 first 和 helper 指针同时 的移动 countNum - 1 for (int i = 0; i < countNum - 1; i++) { first = first.getNext(); helper = helper.getNext(); } // 这时first指向的节点,就是要出圈的小孩节点\ System.out.printf("小孩%d出圈\n", first.getNo()); first = first.getNext(); helper.setNext(first); } System.out.printf("最后留在圈中的小孩编号%d \n", first.getNo()); }