数据结构与算法:核心原理与十大算法
数据结构概述
一、数据结构介绍
算法是程序的灵魂
应用场景 -> 数据结构或算法 -> 剖析原理 -> 分析实现步骤 -> 代码实现
二、数据结构与算法的关系
数据(data)结构(structure)是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。学好数据结构可以编写出更加漂亮、更加有效率的代码。
程序 = 数据结构 + 算法
数据结构是算法的基础
三、线性结构和非线性结构
数据结构分类:线性结构、非线性结构
1、线性结构
线性结构是最常用的数据结构
线性结构的特点:数据元素一一对应
线性结构的两种存储结构:顺序存储结构(元素连续)、链式存储结构(元素不一定连续)
常见的线性结构:数组、队列、链表、栈
2、非线性结构
常见非线性结构:二维数组、多维数组、广义表、树结构、图结构
图
一、图的概述
1、图的概念
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。(结点也可以称为顶点)
2、图的基本概念
- 顶点(vertex)
- 边(edge)
- 路径
- 无向图:顶点之间的连接没有方向
- 有向图:顶点之间的连接有方向
- 带权图(网):边带权值的图
3、图的实现方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)
1)邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵(二维数组),对于n个顶点的图而言,矩阵是的row 和col 表示的是 1..n个点。
2)邻接表
邻接矩阵需要为每个顶点都分配n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失。
邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成。
二、深度优先搜索(dfs)
深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策路就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点。
每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
深度优先搜索是一个递归的过程
三、广度优先搜索(bfs)
图的广度优先搜索(Broad First Search),类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
void bfs(起始点) {
将起始点放入队列中;
标记起点访问;
while(如果队列不为空){
访问队首元素x;
删除队首元素;
for(x的相邻点) {
if(没被标记) {
加入队尾并标记;
}
}
}
队列为空,广搜结束;
}四、图的代码实现
package work.rexhao.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 图的基础及dfs、bfs遍历
* @Date 2022/7/11 22:26
*/
public class Graph {
int[][] edges; // 邻接矩阵
int n; // 节点个数
List<String> vertex;// 顶点名称
boolean[] flag; // 记录是否被访问过
public static void main(String[] args) {
Graph graph = new Graph(5);
String[] strs = {"a", "b", "c", "d", "e"};
graph.addVertex(strs);
graph.addEdges(0, 1, 1); // a -- b
graph.addEdges(0, 2, 1); // a -- c
graph.addEdges(2, 3, 1); // c -- d
graph.addEdges(0, 4, 1); // a -- e
graph.showGraph();
System.out.println("-----------");
graph.dfs();
graph.bfs();
}
/**
* 广度优先遍历
*/
private void bfs() {
flag = new boolean[n];
System.out.print("bfs:");
ArrayQueue queue = new ArrayQueue(n);
queue.addQueue(0);
while (!queue.isEmpty()) {
// 出队列
int i = queue.getQueue();
System.out.print(getValueByIndex(i));
// 标记
flag[i] = true;
// 把该节点连接的节点加入队列
for (int j = 0; j < n; j++) {
if (!flag[j] && edges[i][j] != 0) {
queue.addQueue(j);
}
}
}
System.out.println();
}
/**
* 深度优先遍历
*/
public void dfs() {
flag = new boolean[n];
System.out.print("dfs:");
for (int i = 0; i < n; i++) {
dfs(i);
}
System.out.println();
}
public void dfs(int i) {
// 标记过 -> return
if (flag[i]) {
return;
}
// 没被标记 -> 输出该节点 -> 标记
System.out.print(getValueByIndex(i));
flag[i] = true;
// 找到该节点的后续节点
for (int j = 0; j < n; j++) {
if (edges[i][j] != 0) {
dfs(j);
}
}
}
/**
* 构造器
*
* @param n 节点个数
*/
Graph(int n) {
this.n = n;
edges = new int[n][n];
vertex = new ArrayList<>(n);
}
/**
* 添加节点名称
*
* @param strs 节点名称数组
*/
public void addVertex(String[] strs) {
vertex.addAll(Arrays.asList(strs));
}
/**
* 添加边
*
* @param v1 节点1
* @param v2 节点2
* @param weight 权重
*/
public void addEdges(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
}
/**
* 打印邻接矩阵
*/
public void showGraph() {
for (int[] edge : edges) {
for (int i : edge) {
System.out.print(i + " ");
}
System.out.println();
}
}
/**
* 根据下标获取节点数据
*
* @param i 节点下标
* @return 节点数据
*/
public String getValueByIndex(int i) {
return vertex.get(i);
}
}
class ArrayQueue {
private int maxSize; // 数组最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 队列的数据
/**
* 创建队列的构造器
*/
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[arrMaxSize];
front = -1;// 指向队列头的前一个位置
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;
}
arr[++rear] = n;
}
/**
* 出队列
*/
public int getQueue() {
if (isEmpty()) {
// 抛出异常 -- 不需要写return
throw new RuntimeException("队列空");
}
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\n", i, arr[i]);
}
}
/**
* 显示头数据(不取出)
*/
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空");
}
return arr[front + 1];
}
}树结构应用
一、堆排序
1、堆排序概述
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为 O(nlogn),它也是不稳定排序
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
- 结点的左孩子的值和右孩子的值的大小关系无要求
2、堆排序思路
- 将待排序序列构造成一个大顶堆(数组形式)
- 整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到 n-1 个元素的次小值。如此反复执行,便能得到一个有序序列了
3、堆排序代码实现
package work.rexhao.tree;
import java.util.Arrays;
/**
* 堆排序
* @Date 2022/7/5 21:36
*/
public class HeapSort {
public static void main(String[] args) {
int[] num = new int[]{3, 2, 4, 1, 5};
System.out.println(Arrays.toString(num));
heapSort(num, num.length);
System.out.println(Arrays.toString(num));
}
/**
* 维护大顶堆
*
* @param num 数组
* @param len 数组长度
* @param index 待维护元素的下表
*/
public static void heapify(int[] num, int len, int index) {
int max = index;
// 找到最大值
int leftSon = index * 2 + 1;
int rightSon = index * 2 + 2;
if (leftSon < len && num[max] < num[leftSon]) {
max = leftSon;
}
if (rightSon < len && num[max] < num[rightSon]) {
max = rightSon;
}
// 最大的不是父节点 => 交换
if (max != index) {
// 1.交换
int temp = num[max];
num[max] = num[index];
num[index] = temp;
// 2.继续维护下面的子堆
heapify(num, len, max);
}
}
/**
* 堆排序入口
*
* @param num 待排序数组
* @param len 数组长度
*/
public static void heapSort(int[] num, int len) {
// 建堆
for (int i = len / 2 - 1; i >= 0; i--) {
heapify(num, len, i);
}
// 交换首尾元素
for (int i = len - 1; i > 0; i--) {
int temp = num[i];
num[i] = num[0];
num[0] = temp;
heapify(num, i, 0);
}
}
}二、赫夫曼树
1、赫夫曼树基本介绍
赫夫曼树(Hufriman Tree):给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树
赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
2、赫夫曼树的概念
1)路径和路径长度
在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。
通路中分支的数目称为路径长度。
若规定根结点的层数为1,则从根结点到第工 层结点的路径长度为L-1
2)结点的权及带权路径长度
若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
3)树的带权路径长度
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length)
权值越大的结点离根结点越近的二叉树才是最优二叉树。
4)WPL
WPL 最小的就是赫夫曼树
3、赫夫曼树的代码实现
package work.rexhao.huffmantree;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collections;
/**
* 哈夫曼树的生成
* @Date 2022/7/6 12:49
*/
public class HuffmanTreeDemo {
public static void main(String[] args) {
int num[] = {13, 7, 8, 3, 29, 6, 1};
initHuffmanTree(num);
}
public static void initHuffmanTree(int[] num) {
ArrayList<TreeNode> tnodes = new ArrayList<>();
for (int i : num) {
tnodes.add(new TreeNode(i));
}
while (tnodes.size() != 1) {
Collections.sort(tnodes);
TreeNode leftNode = tnodes.get(0);
TreeNode rightNode = tnodes.get(1);
tnodes.add(new TreeNode(leftNode.getData() + rightNode.getData(), leftNode, rightNode));
tnodes.remove(leftNode);
tnodes.remove(rightNode);
Collections.sort(tnodes);
}
preOrder(tnodes.get(0));
}
/**
* 先序遍历
*/
public static void preOrder(TreeNode tn) {
if (tn != null) {
System.out.print(tn.getData() + " ");
preOrder(tn.leftNode);
preOrder(tn.rightNode);
}
}
}
/**
* 二叉树的节点
*/
class TreeNode implements Comparable<TreeNode> {
private int data;
TreeNode leftNode;
TreeNode rightNode;
TreeNode(int data) {
this.data = data;
}
public TreeNode(int data, TreeNode leftNode, TreeNode rightNode) {
this.data = data;
this.leftNode = leftNode;
this.rightNode = rightNode;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
@Override
public int compareTo(@NotNull TreeNode o) {
return this.data - o.data;
}
}三、赫夫曼编码
1、赫夫曼编码的基本介绍
- 赫夫曼编码是赫哈大曼树在电讯通信中的经典的应用之一
- 赫夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~-90%之间
- 赫夫曼码是可变字长编码(VLC)的一种
- Huffman于1952年提出一种编码方法,称之为最佳编码
- 宇符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码,即不能匹配到重复的编码
2、赫夫曼编码的注意事项
- 赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是wpl是一样的,都是最小的,最后生成的赫夫曼编码的长度是一样
- 如果文件本身就是经过压缩处理的,那么使用赫夫曼编码再压缩效率不会有明显变化,比如视频、ppt 等等文件
- 赫夫曼编码是按字节来处理的,因此可以处理所有的文件(二进制文件、文本文件)
- 如果一个文件中的内容,重复的数据不多,压缩效果也不会很明显
3、代码实现
未解决的Bug:
哈夫曼字节编码8个二进制数一组。 对于最后一组,若0开头,转换后因为不去补零,丢失前面的0;如果补零,最后一组不一定是8位,不能补零。 我的做法是:将最后一组后面补零至8位,但是会造成解码后产生多余字符。
package work.rexhao.huffmantree;
import org.jetbrains.annotations.NotNull;
import java.util.*;
/**
* 哈夫曼编码与解码
* @Date 2022/7/10 16:08
*/
public class HuffmanCode {
// 存储哈夫曼编码表
static Map<Byte, String> HuffmanCodes = new HashMap<>();
public static void main(String[] args) {
String str1 = "i like java java java do you like java?";
System.out.println("str1:" + str1);
byte[] huffmanBytes = huffmanZip(str1.getBytes());
System.out.println("huffmanBytes:" + Arrays.toString(huffmanBytes));
String str2 = huffmanUnzip(huffmanBytes);
System.out.println("str2:" + str2);
}
/**
* 哈夫曼解码
*
* @param huffmanBytes 哈夫曼编码
* @return 解码后字符串
*/
private static String huffmanUnzip(byte[] huffmanBytes) {
StringBuilder code = new StringBuilder();
for (byte b : huffmanBytes) {
code.append(Integer.toBinaryString(b | 256).substring(
Integer.toBinaryString(b | 256).length() - 8));
}
Map<String, Byte> unzipCodes = new HashMap<>();
for (Map.Entry<Byte, String> entry : HuffmanCodes.entrySet()) {
unzipCodes.put(entry.getValue(), entry.getKey());
}
List<Byte> bytesList = new ArrayList<>();
for (int i = 0; i < code.length(); ) {
int count = 1;
while (true) {
if (i + count >= code.length()) {
i = code.length();
break;
}
String key = code.substring(i, i + count);
Byte value = unzipCodes.get(key);
if (value == null) {
count++;
} else {
bytesList.add(value);
i += count;
break;
}
}
}
byte[] bytes = new byte[bytesList.size()];
for (int i = 0; i < bytesList.size(); i++) {
bytes[i] = bytesList.get(i);
}
return new String(bytes);
}
/**
* 哈夫曼编码
*
* @param bytes 编码前
* @return 编码后
*/
public static byte[] huffmanZip(byte[] bytes) {
// 1.创建哈夫曼树
Map<Byte, Integer> huffmanMap = byteCounter(bytes);
HuffmanCodeNode root = initHuffmanTree(huffmanMap);
// 2.获得哈夫曼编码表
toHuffmanCodes(root);
// 3.进行转码
return toHuffmanBytes(bytes, HuffmanCodes);
}
/**
* 通过哈夫曼编码表转化成哈弗曼编码
*
* @param bytes 原字符数组
* @param huffmanCodes 哈夫曼编码表
* @return 编码后的哈夫曼编码
*/
private static byte[] toHuffmanBytes(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(huffmanCodes.get(b));
}
byte[] code = new byte[(sb.length() + 7) / 8];
int index = 0;
for (int i = 0; i < sb.length(); i += 8) {
if (i + 8 < sb.length()) {
code[index++] = (byte) Integer.parseInt(sb.substring(i, i + 8), 2);
} else {
StringBuilder temp = new StringBuilder(sb.substring(i));
for (int j = temp.length(); j < 8; j++) {
temp.append("0");
}
code[index++] = (byte) Integer.parseInt(temp.toString(), 2);
}
}
return code;
}
/**
* 将哈夫曼树转换为哈夫曼编码表(重载)
*
* @param root 根节点
*/
private static void toHuffmanCodes(HuffmanCodeNode root) {
toHuffmanCodes(root, "");
}
/**
* 将哈夫曼树转换为哈夫曼编码表
*
* @param node 节点
* @param s 代拼接编码
*/
private static void toHuffmanCodes(HuffmanCodeNode node, String s) {
if (node == null) {
return;
}
// node有值的话 -> 将b放进去
if (node.b != null) {
HuffmanCodes.put(node.b, s);
}
toHuffmanCodes(node.leftNode, s + "0");
toHuffmanCodes(node.rightNode, s + "1");
}
/**
* 创建哈夫曼树
*
* @param huffmanMap 字符权重
* @return 根节点
*/
private static HuffmanCodeNode initHuffmanTree(Map<Byte, Integer> huffmanMap) {
List<HuffmanCodeNode> list = new ArrayList<>();
for (Byte b : huffmanMap.keySet()) {
list.add(new HuffmanCodeNode(b, huffmanMap.get(b)));
}
while (list.size() != 1) {
Collections.sort(list);
HuffmanCodeNode hcn1 = list.get(0);
HuffmanCodeNode hcn2 = list.get(1);
list.remove(0);
list.remove(0);
list.add(new HuffmanCodeNode(null, hcn1.weight + hcn2.weight, hcn1, hcn2));
}
return list.get(0);
}
/**
* 统计字符出现的次数
*
* @param bytes 待统计字符
* @return 字符权重map
*/
public static Map<Byte, Integer> byteCounter(byte[] bytes) {
Map<Byte, Integer> map = new HashMap<>();
for (byte b : bytes) {
map.merge(b, 1, Integer::sum);
}
return map;
}
}
/**
* 哈夫曼编码的哈夫曼树节点
*/
class HuffmanCodeNode implements Comparable<HuffmanCodeNode> {
Byte b;
int weight;
HuffmanCodeNode rightNode;
HuffmanCodeNode leftNode;
public HuffmanCodeNode(Byte b, int weight) {
this.b = b;
this.weight = weight;
}
public HuffmanCodeNode(Byte b, int weight, HuffmanCodeNode rightNode, HuffmanCodeNode leftNode) {
this.b = b;
this.weight = weight;
this.rightNode = rightNode;
this.leftNode = leftNode;
}
@Override
public int compareTo(@NotNull HuffmanCodeNode o) {
return this.weight - o.weight;
}
@Override
public String toString() {
return "HuffmanCodeNode{" + "b=" + b + ", weight=" + weight + ", rightNode=" + rightNode + ", leftNode=" + leftNode + '}';
}
}四、二叉排序树(BST)
1、二叉排序树概述
二叉排序树:BST(Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。
如果有相同的值,可以将该节点放在左子节点或右子节点
2、二叉排序树删除节点
1)叶子节点
- 找到该节点
- 找到该节点的父节点
- 判断该节点与其父节点的位置
- 左子树:
parentNode.left == null - 右子树:
parentNode.right == null
- 左子树:
2)度为一的节点
- 找到该节点
- 找到该节点的父节点
- 判断该节点与其父节点的位置
- 该节点是父节点的左子树
- 该节点存在左子树:
parentNode.left == node.left - 该节点存在右子树:
parentNode.left == node.right
- 该节点存在左子树:
- 该节点是父节点的右子树
- 该节点存在左子树:
parentNode.right == node.left - 该节点存在右子树:
parentNode.right == node.right
- 该节点存在左子树:
- 该节点是父节点的左子树
3)度为二的节点
- 找到该节点
- 找到该节点右子树的最小节点(递归其右子树的左节点的左节点...)
- 保存最小节点的值temp
- 删除最小节点(叶子结点)
- 将该节点的的值改为temp
3、二叉排序树代码实现
package work.rexhao.binarySortTree;
/**
* 二叉排序树BST
* @Date 2022/7/11 10:05
*/
public class BinarySortTreeDemo {
public static void main(String[] args) {
BinarySortTree bst = new BinarySortTree();
int[] num = {7, 3, 10, 1, 6, 9, 12, 2};
for (int i : num) {
bst.add(new Node(i));
}
BinarySortTree.infixOrder(bst.root);
System.out.println();
bst.delete(10);
BinarySortTree.infixOrder(bst.root);
System.out.println();
}
}
/**
* 二叉排序树
*/
class BinarySortTree {
Node root;
/**
* 添加节点
*
* @param next 待添加节点
*/
public void add(Node next) {
if (root == null) {
root = next;
} else {
root.add(next);
}
}
/**
* 前序遍历
*
* @param node 根节点
*/
public static void infixOrder(Node node) {
if (node == null) {
return;
}
infixOrder(node.left);
System.out.print(node.data + " ");
infixOrder(node.right);
}
/**
* 删除节点
*
* @param target 待删除节点的值
*/
public void delete(int target) {
Node node = search(root, target);
Node nodeParent = searchParent(root, node);
// 经典判空
if (node == null || nodeParent == null) {
System.out.println("节点不存在或节点无父节点!");
return;
}
// 判断待删除节点的形式
if (node.left == null && node.right == null) {
// 叶子节点
if (nodeParent.right == node) {
// 右节点
nodeParent.right = null;
} else {
// 左节点
nodeParent.left = null;
}
} else if (node.left != null && node.right != null) {
// 两个子树的节点
// 1. 找右子树的最小值(右子树的左子树的叶子结点)
Node min = searchRightMinNode(node.right);
// 2. 记录最小值
int temp = min.data;
// 3. 删除原来的最小值节点
delete(min.data);
// 4. 将最小值提到(覆盖)节点位置
node.data = temp;
} else {
// 一个子树的节点
if (node.left == null) {
// 存在右子树
if (nodeParent.right == node) {
// 右节点
nodeParent.right = node.right;
} else {
// 左节点
nodeParent.left = node.right;
}
} else {
// 存在左子树
if (nodeParent.right == node) {
// 右节点
nodeParent.right = node.left;
} else {
// 左节点
nodeParent.left = node.left;
}
}
}
}
/**
* 找 node 节点右子树的最小节点
*
* @param node 开始寻找的节点
* @return 右子树的最小节点
*/
private Node searchRightMinNode(Node node) {
// 找到叶子结点(最小的点)
return node.left == null ? node : searchRightMinNode(node.left);
}
/**
* 找 data 为 i 的节点
*
* @param root 根节点
* @param i 待查找数据
* @return 查找到的节点,不存在返回null
*/
public static Node search(Node root, int i) {
// 1.经典判空
if (root == null) {
return null;
}
// 2.找节点
if (i == root.data) {
return root;
} else if (i < root.data) {
return search(root.left, i);
} else {
return search(root.right, i);
}
}
/**
* 找到 target 的父节点
*
* @param root 根节点
* @param target 待寻找节点
* @return 待寻找节点的父节点,不存在返回null
*/
public static Node searchParent(Node root, Node target) {
// 经典判空
if (root == null || target == null) {
return null;
}
// 判左右的节点
if ((root.right != null && root.right.data == target.data) || (root.left != null && root.left.data == target.data)) {
return root;
}
// 没找到 -> 递归往下找
if (root.data > target.data) {
return searchParent(root.left, target);
} else {
return searchParent(root.right, target);
}
}
}
/**
* 节点
*/
class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
/**
* 添加节点
*
* @param next 待添加节点
*/
public void add(Node next) {
if (next == null) {
return;
}
if (this.data > next.data) {
if (this.left == null) {
this.left = next;
} else {
this.left.add(next);
}
} else {
if (this.right == null) {
this.right = next;
} else {
this.right.add(next);
}
}
}
@Override
public String toString() {
return "Node{" + "data=" + data + '}';
}
}五、平衡二叉树(AVL树)
1、BST可能的问题
若BST左子树or右子树为空 -> 单链表
查询速度明显降低(因为需要依次比较),不能发挥BST的优势
2、平衡二叉树概述
定义:平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为 AVL 树(AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis)
特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
3、平衡二叉树的转换
1)左旋转
- 取右子节点
- 将根节点的值挂在 newRoot 上
- 将 root 的左子节点挂在 newRoot.left.left
- 将原 newRoot 的左子节点挂在 newRoot.left.right
- 将原 newRoot 的右子节点挂在 newRoot.right
- 用 newRoot 替换 root
2)右旋转
- 取左子节点
- 将根节点的值挂在 newRoot 上
- 将 root 的右子节点挂在 newRoot.right.right
- 将原 newRoot 的右子节点挂在 newRoot.right.left
- 将原 newRoot 的左子节点挂在 newRoot.left
- 用 newRoot 替换 root
3)双旋转
左子树 > 右子树:右旋转
左子树 < 右子树:左旋转
4、AVL树的代码实现
package work.rexhao.AVLTree;
import java.util.Arrays;
/**
* 平衡二叉树
*/
public class AVLDemo {
public static void main(String[] args) {
// 案例1:左旋转
int[] num0 = {4, 3, 6, 5, 7, 8};
System.out.println(Arrays.toString(num0));
AVLTree avl0 = new AVLTree();
for (int i : num0) {
avl0.add(new Node(i));
}
avl0.infixOrder();
System.out.println("left = " + avl0.findLeftHeight());
System.out.println("right = " + avl0.findRightHeight());
System.out.println("是否为AVL树:" + avl0.isAVL());
avl0.leftRotate();
avl0.infixOrder();
System.out.println("left = " + avl0.findLeftHeight());
System.out.println("right = " + avl0.findRightHeight());
System.out.println("是否为AVL树:" + avl0.isAVL());
System.out.println("-------------");
// 案例2:右旋转
int[] num1 = {7, 5, 8, 4, 6, 3};
System.out.println(Arrays.toString(num1));
AVLTree avl1 = new AVLTree();
for (int i : num1) {
avl1.add(new Node(i));
}
avl1.infixOrder();
System.out.println("left = " + avl1.findLeftHeight());
System.out.println("right = " + avl1.findRightHeight());
System.out.println("是否为AVL树:" + avl1.isAVL());
avl1.rightRotate();
avl1.infixOrder();
System.out.println("left = " + avl1.findLeftHeight());
System.out.println("right = " + avl1.findRightHeight());
System.out.println("是否为AVL树:" + avl1.isAVL());
System.out.println("-------------");
// 案例3:双旋转
int[] num2 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(Arrays.toString(num2));
AVLTree avl2 = new AVLTree();
for (int i : num2) {
avl2.add(new Node(i));
}
avl2.infixOrder();
System.out.println("left = " + avl2.findLeftHeight());
System.out.println("right = " + avl2.findRightHeight());
System.out.println("是否为AVL树:" + avl2.isAVL());
avl2.rotate();
avl2.infixOrder();
System.out.println("left = " + avl2.findLeftHeight());
System.out.println("right = " + avl2.findRightHeight());
System.out.println("是否为AVL树:" + avl2.isAVL());
System.out.println("-------------");
}
}
/**
* 二叉排序树
*/
class AVLTree {
Node root;
/**
* 双旋转
*/
public void rotate() {
while (true) {
if (isAVL()) {
break;
} else {
if (findLeftHeight() > findRightHeight()) {
rightRotate();
} else {
leftRotate();
}
}
}
}
/**
* 左旋转
*/
public void leftRotate() {
// 0. 经典判空
if (root == null) return;
// 1. 取右子节点
Node newRoot = new Node(root.right.data);
// 2. 将根节点的值挂在 newRoot 上
newRoot.left = new Node(root.data);
// 3. 将 root 的左子节点挂在 newRoot.left.left
newRoot.left.left = root.left;
// 4. 将原 newRoot 的左子节点挂在 newRoot.left.right
newRoot.left.right = root.right.left;
// 5. 将原 newRoot 的右子节点挂在 newRoot.right
newRoot.right = root.right.right;
// 6. 用 newRoot 替换 root
this.root = newRoot;
}
/**
* 右旋转
*/
public void rightRotate() {
// 0. 经典判空
if (root == null) return;
// 1. 取左子节点
Node newRoot = new Node(root.left.data);
// 2. 将根节点的值挂在 newRoot 上
newRoot.right = new Node(root.data);
// 3. 将 root 的右子节点挂在 newRoot.right.right
newRoot.right.right = root.right;
// 4. 将原 newRoot 的右子节点挂在 newRoot.right.left
newRoot.right.left = root.left.right;
// 5. 将原 newRoot 的左子节点挂在 newRoot.left
newRoot.left = root.left.left;
// 6. 用 newRoot 替换 root
this.root = newRoot;
}
/**
* 判断是否为平衡二叉树
*/
public boolean isAVL() {
return Math.abs(findLeftHeight() - findRightHeight()) <= 1;
}
/**
* 找右子树深度
*
* @return 右子树深度
*/
public int findRightHeight() {
if (root == null) {
return 0;
}
int[] r = {1};
int[] l = {1};
findHeight(root.right, r, l);
return Math.max(r[0], l[0]);
}
/**
* 找左子树深度
*
* @return 左子树深度
*/
public int findLeftHeight() {
if (root == null) {
return 0;
}
int[] r = {1};
int[] l = {1};
findHeight(root.left, r, l);
return Math.max(r[0], l[0]);
}
/**
* 求深度
*/
private void findHeight(Node node, int[] r, int[] l) {
// 经典判空
if (node == null) {
return;
}
// 判叶子节点
if (node.left == null && node.right == null) {
return;
}
if (node.left != null) {
r[0]++;
findHeight(node.left, r, l);
}
if (node.right != null) {
l[0]++;
findHeight(node.right, r, l);
}
}
/**
* 添加节点
*
* @param next 待添加节点
*/
public void add(Node next) {
if (root == null) {
root = next;
} else {
root.add(next);
}
}
/**
* 中序遍历
*/
public void infixOrder() {
Node.infixOrder(root);
System.out.println();
}
}
/**
* 节点
*/
class Node {
int data;
Node left;
Node right;
public Node(int data) {
this.data = data;
}
/**
* 添加节点
*
* @param next 待添加节点
*/
public void add(Node next) {
if (next == null) {
return;
}
if (this.data > next.data) {
if (this.left == null) {
this.left = next;
} else {
this.left.add(next);
}
} else {
if (this.right == null) {
this.right = next;
} else {
this.right.add(next);
}
}
}
/**
* 中序遍历
*
* @param node 根节点
*/
public static void infixOrder(Node node) {
if (node == null) {
return;
}
infixOrder(node.left);
System.out.print(node.data + " ");
infixOrder(node.right);
}
@Override
public String toString() {
return "Node{" + "data=" + data + '}';
}
}多路查找树
一、二叉树和B树
1、二叉树的问题
二叉树需要加载到内存的,如果二叉树的节点很多, 就存在问题:
- 在构建二叉树时,需要多次进行 i/o 操作(海量数据存在数据库或文件中),节点海量,构建二叉树时速度有影响
- 节点海量,也会造成二叉树的高度很大,会降低操作速度.
2、多叉树
多叉树(multiway tree):每个节点可以有更多的数据项和更多的子节点
多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
3、B树基本介绍
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率。
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k), 这样每个节点只需要一次 I/O 就可以完全载入
二、2-3树
1、2-3树的特点
- 所有叶子节点都在同一层(只要是 B 树都满足这个条件)
- 2-3 树是由二节点和三节点构成的树。
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点;有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点。
2、2-3树构建规则
- 当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层,拆后仍然需要满足上面 3 个条件。
- 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
3、2-3树构建实例
4、2-3-4树
三、B树、B+树、B*树
1、B树
B-tree 树即 B 树,B 即 Balanced,平衡的意思。(有人把 B-tree 翻译成 B-树,B-tree 就是指的 B 树)
Mysql的某种类型的索引是基于 B 树或者 B+树的
- B 树的阶:节点的最多子节点个数。比如 2-3 树的阶是 3,2-3-4 树的阶是 4尚硅谷 Java 数据结构和算法。
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据。
- 搜索有可能在非叶子结点结束。
- 其搜索性能等价于在关键字全集内做一次二分查找 。
2、B+树
B+树是 B 树的变体,也是一种多路搜索树。
B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【稠密索引】),且链表中的数据是有序的。
非叶子结点相当于是叶子结点的索引【稀疏索引】,叶子结点相当于是存储(关键字)数据的数据层
B+树更适合文件索引系统,B 树和 B+树各有自己的应用场景,不能说 B+树完全比 B 树好,反之亦然
3、B*树
B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针。
B*树定义了非叶子结点关键字个数至少为(2/3)*M,即块的最低使用率为 2/3,而 B+树的块的最低使用率为的 1/2。B*树分配新结点的概率比 B+树要低,空间使用率更高。
十大算法
一、二分查找(非递归)
1、非递归二分查找概述
- 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找
- 二分查找法的运行时间为对数时间 O(㏒₂n),即查找到需要的目标位置最多只需要 ㏒₂n 步
2、非递归二分查找代码实现
package work.rexhao.search;
/**
* 非递归二分查找
* @Date 2022/7/14 11:05
*/
public class BinarySearchNoRecursion {
public static void main(String[] args) {
int[] num = new int[]{10, 12, 23, 32, 45, 64, 65, 69, 83};
System.out.println(binarySearchNoRecursion(10, num));
System.out.println(binarySearchNoRecursion(65, num));
System.out.println(binarySearchNoRecursion(99, num));
}
public static int binarySearchNoRecursion(int target, int[] num) {
int left = 0;
int right = num.length - 1;
int mid;
while (left <= right) {
mid = (left + right) / 2;
if (target == num[mid]) {
return mid;
} else if (target > num[mid]) {
// 目标值比中间值大 --> 向右找
left = mid + 1;
} else {
// 目标值比中间值小 --> 向左找
right = mid - 1;
}
}
return -1;
}
}3、补:递归二分查找代码实现
package work.rexhao.search;
import java.util.Arrays;
/**
* 二分查找
* @Date 2022/7/3 22:54
*/
public class BinarySearch {
public static void main(String[] args) {
int[] num = new int[]{10, 12, 23, 32, 45, 64, 65, 69, 83};
System.out.println(Arrays.toString(num));
System.out.println(binarySearch(num, 69, 0, num.length - 1));
System.out.println(binarySearch(num, 99, 0, num.length - 1));
}
public static int binarySearch(int[] num, int target, int left, int right) {
if (left > right) {
return -1;
}
if (num[(left + right) / 2] == target) {
return (left + right) / 2;
} else if (num[(left + right) / 2] > target) {
return binarySearch(num, target, left, (left + right) / 2);
} else {
return binarySearch(num, target, (left + right) / 2 + 1, right);
}
}
}二、分治算法(DC)
1、分治算法介绍
分治法(Divide-and-Conquer(P))是一种很重要的算法。
字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。
2、分治算法的基本步骤
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
- 合并:将各个子问题的解合并为原问题的解
3、分治算法设计模式
if |p| <= n0
then return(ADHOC(P))
// 将p分解为较小的子问题p1,P2,…,Pk
for if <- 1 to k
do yi <- DC递归解决pi
T <- MERGE(y1,x2,…,vk) 合并子问题
return(T)- 其中
|P|表示问题p的规模 n0为一國值,表示当问题P的规模不超过no时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。- 算法
MERGE(y1,x2,…,vk)是该分治法中的合并子算法,用于将P的子问题p1,p2,…,pk的相应的解y1,x2,…,vk合并为P的解。
4、汉诺塔代码实现
package work.rexhao.algorithm.dc;
import java.util.Collections;
/**
* 汉诺塔
* @Date 2022/7/14 10:10
*/
public class hanoiDemo {
static int count = 0;
public static void main(String[] args) {
hanoiTower(5, 'A', 'B', 'C');
}
private static void hanoiTower(int num, char a, char b, char c) {
// 如果只有一个盘
if (num == 1) {
System.out.println(++count + " : 第 1 个盘从 " + a + "->" + c);
} else {
// 如果我们有 n >= 2 情况,我们总是可以看做是两个盘 1.最下边的一个盘 2. 上面的所有盘
// 1. 先把 最上面的所有盘 A->B, 移动过程会使用到 c
hanoiTower(num - 1, a, c, b);
// 2. 把最下边的盘 A->C
System.out.println(++count + " : 第 " + num + " 个盘从 " + a + "->" + c);
// 3. 把 B 塔的所有盘 从 B->C , 移动过程使用到 a 塔
hanoiTower(num - 1, b, a, c);
}
}
}三、动态规划(DP)
1、动态规划算法介绍
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)
- 动态规划可以通过填表的方式来逐步推进,得到最优解。
2、背包问题
1)背包问题概述
有一个背包,容量为4kg,现有如下物品。要求达到的目标为装入的背包的总价值最大,并且重量不超出装入的物品不能重复。
| 物品 | 重量 | 价值 |
|---|---|---|
| G | 1 | 15 |
| S | 4 | 30 |
| L | 3 | 20 |
2)背包问题思路分析
背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分 01 背包和完全背包(完全背包指的是:每种物品都有无限件可用,且无限背包可以转化为 01 背包)
算法的主要思想,利用动态规划来解决。每次遍历到的第 i 个物品,根据 w[i]和 v[i]来确定是否需要将该物品放入背包中。即对于给定的 n 个物品,设 v[i]、w[i]分别为第 i 个物品的价值和重量,C 为背包的容量。再令 v[i][j]表示在前 i 个物品中能够装入容量为 j 的背包中的最大价值。
v[i][0]=v[0][j]=0;表示填入表第一行和第一列是 0- 当
w[i]> j:v[i][j]=v[i-1][j]当准备加入新增的商品的容量大于 当前背包的容量时,就直接使用上一个 单元格的装入策略 - 当
j>=w[i]时:v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]}当准备加入的新增的商品的容量小于等于当前背包的容量, - 装入的方式:
v[i-1][j]: 就是上一个单元格的装入的最大值v[i]:表示当前商品的价值v[i-1][j-w[i]]: 装入 i-1 商品,到剩余空间 j-w[i]的最大值- 当
j>=w[i]:v[i][j]=max{v[i-1][j], v[i]+v[i-1][j-w[i]]} :
3)背包问题代码实现
package work.rexhao.algorithm.dynamic;
/**
* 01背包问题
* @Date 2022/7/14 10:07
*/
public class KnapsackProblem {
public static void main(String[] args) {
// 1. 定义二维数组
int knapsackSize = 4; // 背包容量
int goodsType = 3; // 物品种类
int[][] value = new int[knapsackSize + 1][goodsType + 1]; // dp价值表
int[] goodsSize = {0, 1, 4, 3}; // 每个物品的大小
int[] goodsValue = {0, 15, 30, 20}; // 每个物品的价值
// 2. 第一行为空(空背包)且第一列为空(背包容量为0)
// 3. 遍历表
for (int i = 1; i < knapsackSize + 1; i++) {
// i:背包容量
for (int j = 1; j < goodsType + 1; j++) {
// j:物品类型
knapsackProblem(knapsackSize, i, j, value, goodsSize, goodsValue);
}
}
// 3.1 打印表
for (int i = 0; i < goodsType + 1; i++) {
for (int j = 0; j < knapsackSize + 1; j++) {
System.out.print(value[j][i] + "\t");
}
System.out.println();
}
System.out.println("--------------------");
// 4. 输出答案
int ans = 0;
for (int[] ints : value) {
for (int i : ints) {
ans = Math.max(ans, i);
}
}
System.out.println("总价值最大为: " + ans);
}
private static void knapsackProblem(int knapsackSize, int i, int j, int[][] value, int[] goodsSize, int[] goodsValue) {
if (goodsSize[j] == i) {
// 放一个正好放满 --> 放(这个或上一个品种)中价值更高的
value[i][j] = Math.max(goodsValue[j], value[i][j - 1]);
} else if (goodsSize[j] > i) {
// 这个放不下 --> 放上一品种的
value[i][j] = value[i][j - 1];
} else {
// 放下了,但是剩下位置了 --> 剩下位置找本行相应的那个 --> 放(这个或上一个品种)中价值更高的
value[i][j] = Math.max(goodsValue[j] + value[i - goodsSize[j]][j], value[i][j - 1]);
}
}
}
/*
问题:有一个背包,容量为 4kg, 现有如下物品
要求达到的目标为装入的背包的总价值最大,并且重量不超出
G 1kg 15
S 4kg 30
L 3Kg 20
*/3、最少步数问题
1)最少步数问题概述
河上有一排n个荷叶(编号依此为1,2,3····n),第i个荷叶上有一个整数ai,现在有一只小青蛙在第1个荷叶上,荷叶上的整数表示小青蛙在该位置可以向前跳跃最多的荷叶个数。求小青蛙从第1个荷叶跳到第n个荷叶用的最少的步数为小饼干的考试次数。
https://ac.nowcoder.com/acm/contest/25592/F
2)最少步数问题思路分析
建立step[]数组,存放到达相应位置最少的步数
3)最少步数问题代码实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] step = new int[n + 1];
for (int i = 1; i <= n; i++) step[i] = 99999;
step[1] = 0;
for (int i = 1; i <= n; i++) {
int t = sc.nextInt();
for (int j = 1; j <= t; j++) {
if (i + j > n) continue;
step[i + j] = Math.min(step[i + j], step[i] + 1);
}
}
System.out.println(step[n]);
}
}四、KMP算法
1、字符串匹配问题
有一个字符串 str1 = "i love java do you like java",和一个子串 str2 ="java"
现在要判断 str1 是否含有 str2。如果存在,就返回第一次出现的位置。没有则返回-1。
2、暴力破解(BF)代码实现
package work.rexhao.algorithm.KMP;
/**
* 字符串匹配问题暴力破解
* @Date 2022/7/14 11:12
*/
public class BF {
public static void main(String[] args) {
String str1 = "i love java do you like java";
String str2 = "java";
System.out.println(bf(str1, str2));
}
private static int bf(String str1, String str2) {
int len1 = str1.length();
int len2 = str2.length();
for (int i = 0; i < len1; i++) {
int temp = i;
for (int j = 0; j < len2; j++) {
if (str1.charAt(i) == str2.charAt(j)) {
i++;
if (j == len2 - 1) {
return temp;
}
} else {
i = temp;
break;
}
}
}
return -1;
}
}3、KMP介绍
- 字符串查找算法(Knuth-Morris-Pratt),简称为 “KMP 算法”,常用于在一个文本串 S 内查找一个模式串 P 的出现位置,这个算法由 Donald Knuth、Vaughan Pratt、James H. Morris 三人于 1977 年联合发表,故取这 3 人的姓氏命名此算法
- KMP 方法算法就利用之前判断过信息,通过一个 next 数组,保存模式串中前后最长公共子序列的长度,每次回溯时,通过 next 数组找到,前面匹配过的位置,省去了大量的计算时间
4、KMP思路分析
...
5、KMP代码实现
package work.rexhao.algorithm.KMP;
import java.util.Arrays;
/**
* KMP算法
* @Date 2022/7/15 19:56
*/
public class KMP {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
System.out.println(kmpSearch(str1, str2));
}
public static int[] kmpNext(String target) {
int[] next = new int[target.length()];
next[0] = 0;
int j = 0;
for (int i = 1; i < target.length(); i++) {
if (target.charAt(i) == target.charAt(j)) {
j++;
} else {
if (j > 0) {
j = next[j - 1];
}
}
next[i] = j;
}
return next;
}
public static int kmpSearch(String str1, String str2) {
int[] next = kmpNext(str2);
int j = 0;
for (int i = 0; i < str1.length(); i++) {
if (str1.charAt(i) == str2.charAt(j)) {
j++;
} else {
if (j > 0) {
j = next[j - 1];
}
}
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
}五、贪心算法
1、贪心算法介绍
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
2、集合覆盖问题
假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号
| 广播台 | 覆盖地区 |
|---|---|
| K1 | "北京", "上海", "天津" |
| K2 | "广州", "天津", "深圳" |
| K3 | "成都", "上海", "杭州" |
| K4 | "上海", "天津" |
| K5 | "杭州", "大连" |
- 目前并没有算法可以快速计算得到准备的值, 使用贪婪算法,则可以得到非常接近的解,并且效率高。选择策略上,因为需要覆盖全部地区的最小集合。
- 遍历所有的广播电台, 找到一个覆盖了最多未覆盖的地区的电台(此电台可能包含一些已覆盖的地区,但没有关系。
- 将这个电台加入到一个集合中, 想办法把该电台覆盖的地区在下次比较时去掉。
- 重复第 1 步直到覆盖了全部的地区。
贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果
3、集合覆盖问题代码实现
package work.rexhao.algorithm.greedy;
import java.util.*;
/**
* 贪心算法
* @Date 2022/7/14 19:58
*/
public class GreedyAlgorithm {
public static void main(String[] args) {
/*
K1 "北京", "上海", "天津"
K2 "广州", "天津", "深圳"
K3 "成都", "上海", "杭州"
K4 "上海", "天津"
K5 "杭州", "大连"
*/
String[][] area = {{"北京", "上海", "天津"}, {"广州", "天津", "深圳"},
{"成都", "上海", "杭州"}, {"上海", "天津"}, {"杭州", "大连"}};
HashSet<String> allPlace = new HashSet<>();
HashMap<String, HashSet<String>> map = new HashMap<>();
for (String[] strs : area) {
allPlace.addAll(Arrays.asList(strs));
}
for (int i = 0; i < 5; i++) {
map.put("K" + (i + 1), new HashSet<>(Arrays.asList(area[i])));
}
ArrayList<String> ansList = new ArrayList<>();
while (!allPlace.isEmpty()) {
int maxValue = 0, maxIndex = 0;
for (int i = 0; i < map.size(); i++) {
HashSet<String> set = map.get("K" + (i + 1));
int thisValue = 0;
for (String s : set) {
if (allPlace.contains(s)) thisValue++;
}
if (thisValue > maxValue) {
maxIndex = i;
maxValue = thisValue;
}
}
HashSet<String> maxSet = map.get("K" + (maxIndex + 1));
for (String s : maxSet) {
allPlace.remove(s);
}
ansList.add("K" + (maxIndex + 1));
}
System.out.println(Arrays.toString(ansList.toArray()));
}
}六、普利姆算法(Prim)
作者对于二者算法(克鲁斯卡尔算法与普利姆算法)的了解有待提升
代码实现仅供参考
1、最小生成树
最小生成树(Minimum Cost Spanning Tree),简称MST。一个带权的无向连通图,选取一棵使树上所有边上权的总和为最小的生成树。
- N个顶点,一定有N-1条边
- 包含全部顶点
- N-1条边都在图中
2、普利姆算法概要
普利姆(Prim)算法求最小生成树,也就是在包含 n 个顶点的连通图中,找出只有(n-1)条边包含所有 n 个顶点的连通子图,也就是所谓的极小连通子图
3、修路问题
有 7 个村庄(A, B, C, D, E, F, G),如何修路保证各个村庄都能连通,并且总的修建公路总里程最短?
4、普利姆算法代码实现
package work.rexhao.algorithm.prim;
import com.sun.xml.internal.bind.v2.schemagen.xmlschema.TypeHost;
import java.util.*;
/**
* 普利姆算法
* @Date 2022/7/14 21:00
*/
public class PrimAlgorithm {
public static void main(String[] args) {
String[] nodeName = {"A", "B", "C", "D", "E", "F", "G"};
Graph graph = new Graph(7, nodeName);
System.out.println("最短路径为: " + prim(graph));
}
public static boolean isEmpty(boolean[] b) {
for (boolean B : b) {
if (!B) {
return true;
}
}
return false;
}
private static int prim(Graph graph) {
int ans = 0;
// 找一个开始点 --> A
boolean[] flag = new boolean[graph.n];
flag[0] = true;
// 遍历其他点
while (isEmpty(flag)) {
int minValue = 10000;
int[] minIndex = new int[2];
for (int i = 0; i < graph.edges.length; i++) {
for (int j = 0; j < graph.edges[i].length; j++) {
if (graph.edges[i][j] == 0) continue;
if (flag[i] && !flag[j] && graph.edges[i][j] < minValue) {
minValue = graph.edges[i][j];
minIndex[0] = i;
minIndex[1] = j;
}
}
}
flag[minIndex[0]] = flag[minIndex[1]] = true;
ans += minValue;
}
return ans;
}
}
class Graph {
int[][] edges; // 邻接矩阵
int n; // 节点个数
List<String> node; // 节点名称
public Graph(int n, String[] nodeName) {
this.edges = new int[n][n];
this.n = n;
this.node = new ArrayList<>(Arrays.asList(nodeName));
this.init();
}
private void init() {
/*
"A" 0
"B" 1
"C" 2
"D" 3
"E" 4
"F" 5
"G" 6
*/
// A-B:5 A-C:7 A-G:2
add(0, 1, 5).add(0, 2, 7).add(0, 6, 2);
// F-E:5 F-D:4 F-G:6
add(5, 4, 5).add(5, 3, 4).add(5, 6, 6);
// E-G:4 E-C:8
add(4, 6, 4).add(4, 2, 8);
// B-D:9 B-G:3
add(1, 3, 9).add(1, 6, 3);
}
public Graph add(int i, int j, int weight) {
edges[i][j] = edges[j][i] = weight;
return this;
}
}七、克鲁斯卡尔算法(Kruskal)
1、克鲁斯卡尔算法概要
克鲁斯卡尔(Kruskal)算法,是用来求加权连通图的最小生成树的算法。
基本思想:按照权值从小到大的顺序选择 n-1 条边,并保证这 n-1 条边不构成回路
具体做法:首先构造一个只含 n 个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止
2、Kruskal与Prim的区别
- Prim算法是直接查找,多次寻找邻边的权重最小值;Kruskal算法采用贪心策略,是需要先对权重排序后查找的。
- Kruskal算法在效率上比Prim算法快,因为Krusal算法只要对所有边排序一次就能找到最小生成树;而Prim算法需要对邻边进行多次排序才能找到。
- Prim算法:选择一个顶点作为树的根节点,然后找到以这个点为邻边的最小权重的点,然后将其加入最小生成树中,再重复查找这棵最小生成树的权重最小的边的点,加入其中。(如果加入要产生回路,就跳过这条边)。当所有结点加入最小生成树中,就找到了这个连通图的最小生成树。
- Kruskal算法:利用贪心策略,再找最小生成树结点之前,需要对边的权重从小到大排序,将排序好的权重边依次加入到最小生成树中(如果加入时产生回路就跳过这条边,加入下一条边),当加入的边数为n-1时,就找到了这个连通图的最小生成树。
- Prim算法适合边稠密图,时间复杂度O(n²)
- Kruskal算法与边有关,适合于稀疏图O(eloge)
3、克鲁斯卡尔算法代码实现
package work.rexhao.algorithm.kruskal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
/**
* 克鲁斯卡尔算法
* @Date 2022/7/14 22:42
*/
public class KruskalAlgorithm {
public static void main(String[] args) {
String[] nodeName = {"A", "B", "C", "D", "E", "F", "G"};
Graph graph = new Graph(7, nodeName);
System.out.println("最短路径为: " + prim(graph));
}
private static int prim(Graph graph) {
int ans = 0;
HashSet<String> set = new HashSet<>(graph.node);
while (!set.isEmpty()) {
int minValue = 10000;
int[] minIndex = new int[2];
for (int i = 0; i < graph.edges.length; i++) {
for (int j = 0; j < graph.edges[i].length; j++) {
// 除了第一次,下一个节点必须是连在之前存在的节点上
if (ans != 0) {
if ((!set.contains(graph.node.get(i)) || set.contains(graph.node.get(j)) && (!set.contains(graph.node.get(j)) || set.contains(graph.node.get(i))))) {
continue;
}
}
// 找最小值
if (graph.edges[i][j] != 0 && graph.edges[i][j] < minValue) {
minIndex[0] = i;
minIndex[1] = j;
minValue = graph.edges[i][j];
}
}
}
graph.add(minIndex[0], minIndex[1], 0);
set.remove(graph.node.get(minIndex[0]));
set.remove(graph.node.get(minIndex[1]));
ans += minValue;
}
return ans;
}
}
class Graph {
int[][] edges; // 邻接矩阵
int n; // 节点个数
List<String> node; // 节点名称
public Graph(int n, String[] nodeName) {
this.edges = new int[n][n];
this.n = n;
this.node = new ArrayList<>(Arrays.asList(nodeName));
this.init();
}
private void init() {
/*
"A" 0
"B" 1
"C" 2
"D" 3
"E" 4
"F" 5
"G" 6
*/
// A-B:5 A-C:7 A-G:2
add(0, 1, 5).add(0, 2, 7).add(0, 6, 2);
// F-E:5 F-D:4 F-G:6
add(5, 4, 5).add(5, 3, 4).add(5, 6, 6);
// E-G:4 E-C:8
add(4, 6, 4).add(4, 2, 8);
// B-D:9 B-G:3
add(1, 3, 9).add(1, 6, 3);
}
public Graph add(int i, int j, int weight) {
edges[i][j] = edges[j][i] = weight;
return this;
}
}八、迪杰斯特拉算法(Dijkstra)
1、最短路径问题
如何计算出 G 村庄到 其它各个村庄的最短距离?
2、迪杰斯特拉算法概要
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止。
3、迪杰斯特拉算法基本思想
- 每次从未标记的节点中选择距离出发点最近的节点,标记,收录到最优路径集合中。
- 计算刚加入节点A的邻近节点B的距离 (不包含标记的节点),若
(节点A的距离+节点A到节点B的边长)< 节点B的距离,就更新节点B的距离和前面点。
4、迪杰斯特拉算法代码实现
package work.rexhao.algorithm.dijkstra;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 迪杰斯特拉算法
* @Date 2022/7/15 01:15
*/
public class DijkstraAlgorithm {
public static void main(String[] args) {
// 数据初始化
String[] nodeName = {"A", "B", "C", "D", "E", "F", "G"};
DijkstraForm[] df = new DijkstraForm[7];
for (int i = 0; i < 7; i++) {
df[i] = new DijkstraForm(i);
}
Graph graph = new Graph(7, nodeName);
// 调用dijkstra方法
df[6].pre = 6;
df[6].len = 0;
dijkstra(df, graph, nodeName, 6); //G
// 格式化输出
for (int i = 0; i < 7; i++) {
System.out.println("G --> " + nodeName[i] + " : " + "len = " + df[i].len + " , pre = " + nodeName[df[i].pre]);
}
}
public static void dijkstra(DijkstraForm[] df, Graph graph, String[] nodeName, int i) {
// 1. 判结束
boolean flag = true;
for (DijkstraForm dijkstraForm : df) {
if (!dijkstraForm.flag) {
flag = false;
break;
}
}
if (flag) return;
// 2. 标记自己
df[i].flag = true;
// 3. 更新相邻的表
for (int j = 0; j < graph.edges.length; j++) {
if (graph.edges[i][j] != 0 && !df[j].flag
&& graph.edges[i][j] + df[i].len < df[j].len) {
df[j].len = graph.edges[i][j] + df[i].len;
df[j].pre = i;
}
}
// 4. 递归最小的相邻表
int minValue = 10000;
int minIndex = -1;
for (int j = 0; j < df.length; j++) {
if (df[j].flag) continue;
if (minValue > df[j].len) {
minIndex = j;
minValue = df[j].len;
}
}
dijkstra(df, graph, nodeName, minIndex);
}
}
class DijkstraForm {
int n;
int pre;
int len; // 路径长度
boolean flag; // 标记
public DijkstraForm(int n) {
this.n = n;
this.len = 100000;
this.flag = false;
}
}
class Graph {
int[][] edges; // 邻接矩阵
int n; // 节点个数
List<String> node; // 节点名称
public Graph(int n, String[] nodeName) {
this.edges = new int[n][n];
this.n = n;
this.node = new ArrayList<>(Arrays.asList(nodeName));
this.init();
}
private void init() {
/*
"A" 0
"B" 1
"C" 2
"D" 3
"E" 4
"F" 5
"G" 6
*/
// A-B:5 A-C:7 A-G:2
add(0, 1, 5).add(0, 2, 7).add(0, 6, 2);
// F-E:5 F-D:4 F-G:6
add(5, 4, 5).add(5, 3, 4).add(5, 6, 6);
// E-G:4 E-C:8
add(4, 6, 4).add(4, 2, 8);
// B-D:9 B-G:3
add(1, 3, 9).add(1, 6, 3);
}
public Graph add(int i, int j, int weight) {
edges[i][j] = edges[j][i] = weight;
return this;
}
}G --> A : len = 2 , pre = G G --> B : len = 3 , pre = G G --> C : len = 9 , pre = A G --> D : len = 10 , pre = F G --> E : len = 4 , pre = G G --> F : len = 6 , pre = G G --> G : len = 0 , pre = G
九、弗洛伊德算法(Floyd)
1、弗洛伊德算法介绍
和 Dijkstra 算法一样,弗洛伊德(Floyd)算法也是一种用于寻找给定的加权图中顶点间最短路径的算法。该算法名称以创始人之一、1978 年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名
弗洛伊德算法 VS 迪杰斯特拉算法:
迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径。
弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。
2、弗洛伊德算法思路分析
- 设置顶点 vi 到顶点 vk 的最短路径已知为 Lik,顶点 vk 到 vj 的最短路径已知为 Lkj,顶点 vi 到 vj 的路径为 Lij,则 vi 到 vj 的最短路径为:min((Lik+Lkj),Lij),vk 的取值为图中所有顶点,则可获得 vi 到 vj 的最短路径
- 至于 vi 到 vk 的最短路径 Lik 或者 vk 到 vj 的最短路径 Lkj,是以同样的方式获得
3、弗洛伊德算法代码实现
package work.rexhao.algorithm.floyd;
import java.util.*;
/**
* 弗洛伊德算法
* @Date 2022/7/15 10:50
*/
public class FloydAlgorithm {
static int[][] len;
static int[][] pre;
public static void main(String[] args) {
String[] nodeName = {"A", "B", "C", "D", "E", "F", "G"};
Graph graph = new Graph(7, nodeName);
floyd(graph);
}
public static void floyd(Graph graph) {
// 1. 对len表进行初始化
len = new int[graph.n][graph.n];
for (int i = 0; i < graph.n; i++) {
for (int j = 0; j < graph.n; j++) {
if (i == j) {
len[i][j] = 0;
} else if (graph.edges[i][j] != 0) {
len[i][j] = graph.edges[i][j];
} else {
len[i][j] = 10000;
}
}
}
// 2. 对pre表初始化
pre = new int[graph.n][graph.n];
for (int i = 0; i < graph.n; i++) {
for (int j = 0; j < graph.n; j++) {
pre[i][j] = i;
}
}
// 3. 弗洛伊德算法
// 3.1 i : 中间节点
for (int i = 0; i < graph.n; i++) {
// 3.2 j : 起始节点
for (int j = 0; j < graph.n; j++) {
// 3.3 k : 目的节点
for (int k = 0; k < graph.n; k++) {
int temp = len[j][i] + len[k][i];
if (temp < len[j][k]) {
len[j][k] = temp;
pre[j][k] = i;
}
}
}
}
// 4. 输出测试
for (int[] ints : len) {
System.out.println(Arrays.toString(ints));
}
System.out.println("-------------");
for (int[] p : pre) {
System.out.println(Arrays.toString(p));
}
System.out.println("-------------");
}
}
class Graph {
int[][] edges; // 邻接矩阵
int n; // 节点个数
List<String> node; // 节点名称
public Graph(int n, String[] nodeName) {
this.edges = new int[n][n];
this.n = n;
this.node = new ArrayList<>(Arrays.asList(nodeName));
this.init();
}
private void init() {
/*
"A" 0
"B" 1
"C" 2
"D" 3
"E" 4
"F" 5
"G" 6
*/
// A-B:5 A-C:7 A-G:2
add(0, 1, 5).add(0, 2, 7).add(0, 6, 2);
// F-E:5 F-D:4 F-G:6
add(5, 4, 5).add(5, 3, 4).add(5, 6, 6);
// E-G:4 E-C:8
add(4, 6, 4).add(4, 2, 8);
// B-D:9 B-G:3
add(1, 3, 9).add(1, 6, 3);
}
public Graph add(int i, int j, int weight) {
edges[i][j] = edges[j][i] = weight;
return this;
}
}十、马踏棋盘算法(DFS)
1、马踏棋盘介绍
将马随机放在国际象棋的8×8棋盘的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格
2、马踏棋盘问题的思路分析
- 创建棋盘map,是一个二维数组
- 将当前位蛋设蛋为已经访问,向8个方向中可以继续前进的方向上递归调用,如果走通,就继续;走不通,就回溯
- 判断马儿是否完成了任务,使用count和应该走的步数比较
3、马踏棋盘问题代码实现
package work.rexhao.algorithm.horse;
import java.util.Arrays;
import java.util.Date;
/**
* 马踏棋盘问题
* @Date 2022/7/15 11:29
*/
public class HorseChessboard {
static int[][] dir = {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}, {2, 1}, {2, -1}, {-2, 1}, {-2, -1}};
static boolean[][] vis = new boolean[8][8];
static int[][] map = new int[8][8];
static boolean over;
public static void main(String[] args) {
Date date0 = new Date();
long begin = date0.getTime();
dfs(3, 3, 1);
Date date1 = new Date();
long end = date1.getTime();
System.out.println("运行时间:" + (end - begin) + "ms"); //1100
System.out.println("----------------");
for (int[] ints : map) {
System.out.println(Arrays.toString(ints));
}
}
public static void dfs(int x, int y, int count) {
if (over || count > 64) {
return;
}
if (count == 64) {
over = true;
return;
}
vis[x][y] = true;
map[x][y] = count;
int tx, ty;
for (int[] ints : dir) {
tx = x + ints[0];
ty = y + ints[1];
if (ty >= 0 && tx >= 0 && ty < 8 && tx < 8 && !vis[tx][ty]) {
dfs(tx, ty, count + 1);
}
}
vis[x][y] = false;
}
}稀疏矩阵和队列
一、稀疏数组sparsearray
1、应用场景
需求:五子棋”存盘退出“ --> 二维数组保存
问题:这个二维数组大部分都是默认值0,记录了很多没有意义的数据 --> 稀疏数组
2、稀疏数组
稀疏数组:数组大部分为同一个值
处理方法:记录数组几行几列,有多少个的值。把不同值的行和列记录在小规模数组中。
| 行号row | 列号col | 值value | |
|---|---|---|---|
| [0] | 2 | 2 | 2 |
| [1] | 0 | 1 | 1 |
| [2] | 1 | 0 | 1 |
[0]:共几行几列,存在几个值
[1] - [n]:记录x行x列的值是多少
3、思路分析
1)二维转稀疏
- 遍历原始二维数组,得到有效数据个数 -> sum
- 创建稀疏数组
sparseArr int[sum][3] - 将有效数据存到sparseArr中
2)稀疏转二维
- 读取sparseArr的第一行的数据创建原始二维数组
- 读取sparseArr后面的数值,赋值给二维数组
4、代码实现
1)基础代码
package work.rexhao.sparsearray;
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;
// 输出
System.out.println("原始二维数组");
for(int[] row : chessArr1) {
for(int i : row) {
System.out.printf("%d ",i);
}
System.out.println();
}
// 将二维数组转换成稀疏数组
// 1.遍历二维数组,得到非0个数、行和列
int r = 0, l = 0, v = 0;
for(int[] row : chessArr1) {
r++;
for(int i : row) {
l++;
if(i != 0) {
v++;
}
}
}
l /= r;
// 2.创建稀疏数组
int sparseArr[][] = new int[v+1][3];
// 3.给稀疏数组赋值
sparseArr[0][0] = r;
sparseArr[0][1] = l;
sparseArr[0][2] = v;
int ii = 0;
for(int i = 0; i < r; i++) {
for(int j = 0; j < l; j++) {
if(chessArr1[i][j] != 0) {
sparseArr[++ii][0] = i;
sparseArr[ii][1] = j;
sparseArr[ii][2] = chessArr1[i][j];
}
}
}
// 稀疏数组的输出
System.out.println("得到的稀疏数组是:");
for(int[] row : sparseArr) {
for(int i : row) {
System.out.print(i + " ");
}
System.out.println();
}
// 将稀疏数组恢复二维数组
// 1.读取第一行,创建二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
// 2.读取并赋值
for(int i = 1; i <= sparseArr[0][2]; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
// 输出
System.out.println("转换的二维数组是:");
for(int[] row : chessArr2) {
for(int i : row) {
System.out.printf("%d ",i);
}
System.out.println();
}
}
}2)IO代码
package work.rexhao.sparsearray;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class sparsearray_out {
public static void main(String[] args) throws FileNotFoundException, IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./chess.data"));
// 创建一个原始的二维数组11*11
// 0:没有旗子,1:黑子,2:蓝子
int chessArr1[][] = new int[11][11];
chessArr1[1][2] = 1;
chessArr1[1][3] = 2;
// 将二维数组转换成稀疏数组
// 1.遍历二维数组,得到非0个数、行和列
int r = 0, l = 0, v = 0;
for (int[] row : chessArr1) {
r++;
for (int i : row) {
l++;
if (i != 0) {
v++;
}
}
}
l /= r;
// 2.创建稀疏数组
int sparseArr[][] = new int[v + 1][3];
// 3.给稀疏数组赋值
sparseArr[0][0] = r;
sparseArr[0][1] = l;
sparseArr[0][2] = v;
int ii = 0;
for (int i = 0; i < r; i++) {
for (int j = 0; j < l; j++) {
if (chessArr1[i][j] != 0) {
sparseArr[++ii][0] = i;
sparseArr[ii][1] = j;
sparseArr[ii][2] = chessArr1[i][j];
}
}
}
// 4.序列化
oos.writeObject(sparseArr);
oos.close();
System.out.println("存档成功!");
}
}package work.rexhao.sparsearray;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class sparsearray_in {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("./chess.data"));
int sparseArr[][] = (int[][])ois.readObject();
System.out.println("读档成功!");
// 将稀疏数组恢复二维数组
// 1.读取第一行,创建二维数组
int chessArr2[][] = new int[sparseArr[0][0]][sparseArr[0][1]];
// 2.读取并赋值
for (int i = 1; i <= sparseArr[0][2]; i++) {
chessArr2[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
// 输出
for (int[] row : chessArr2) {
for (int i : row) {
System.out.printf("%d ", i);
}
System.out.println();
}
}
}二、队列
1、队列概述
队列是一个有序列表,可以用数组或链表实现
遵循先入先出原则
2、数组模拟队列
1)数组结构介绍
数组的结构存储队列元素,maxSize是队列最大容量
两个变量front和rear记录队列前后端
2)思路分析
- 尾指针后移:rear+1
- rear < maxSize - 1:可以存
当front == rear,队列空
当rear == maxSize - 1,队列满
3)代码实现
package work.rexhao.queue;
import java.util.Scanner;
public class arrayQueueDemo {
public static void main(String[] args) {
ArrayQueue aq = new ArrayQueue(3);
boolean loop = true;
while (loop) {
Scanner sc = new Scanner(System.in);
System.out.println("1. 添加数据");
System.out.println("2. 弹出数据");
System.out.println("3. 显示首数据");
System.out.println("4. 遍历");
System.out.println("0. 退出");
int key = sc.nextInt();
if (key == 1) {
System.out.println("请输入需要添加的数据");
int value = sc.nextInt();
aq.addQueue(value);
} else if (key == 2) {
try {
int value = aq.getQueue();
System.out.printf("弹出元素:%d\n",value);
} catch (Exception e) {
System.out.println(e.getMessage());
}
} else if (key == 3) {
try {
int value = aq.headQueue();
System.out.printf("首元素:%d\n", value);
} catch (Exception e) {
System.out.println(e.getMessage());
}
} else if (key == 4) {
aq.showQueue();
} else if (key == 0) {
loop = false;
} else {
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[arrMaxSize];
front = -1;// 指向队列头的前一个位置
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;
}
arr[++rear] = n;
}
/**
* 出队列
*/
public int getQueue() {
if (isEmpty()) {
// 抛出异常 -- 不需要写return
throw new RuntimeException("队列空");
}
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\n", i, arr[i]);
}
}
/**
* 显示头数据(不取出)
*/
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空");
}
return arr[front + 1];
}
}4)问题与优化
- 对于普通队列,数组使用一次就不能用,没有达到复用的效果
- 将这个数组使用算法,改进成一个环形的队列 (取模:%)
3、数组模拟环形队列
1)思路分析
- front变量的含叉做一个调整:front就指向队列的第一个元素,也就是说ari[front] 就是队列的第一个元素。front 的初始值=0
- rear 变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,空出一个空间做为约定。rear 的初始值=0
- 当队列满时,条件是
(rear +1) % maxsize == front - 对队列为空的条件,
rear == front - 队列中有效的数据的个数
(rear+ maxsize - front) % maxsize
2)代码实现
package work.rexhao.queue;
import java.util.Scanner;
public class circleQueueDemo {
public static void main(String[] args) {
CircleQueue cq = new CircleQueue(3);
boolean loop = true;
while (loop) {
Scanner sc = new Scanner(System.in);
System.out.println("1. 添加数据");
System.out.println("2. 弹出数据");
System.out.println("3. 显示首数据");
System.out.println("4. 遍历");
System.out.println("0. 退出");
int key = sc.nextInt();
if (key == 1) {
System.out.println("请输入需要添加的数据");
int value = sc.nextInt();
cq.addQueue(value);
} else if (key == 2) {
try {
int value = cq.getQueue();
System.out.printf("弹出元素:%d\n", value);
} catch (Exception e) {
System.out.println(e.getMessage());
}
} else if (key == 3) {
try {
int value = cq.headQueue();
System.out.printf("首元素:%d\n", value);
} catch (Exception e) {
System.out.println(e.getMessage());
}
} else if (key == 4) {
cq.showQueue();
} else if (key == 0) {
loop = false;
} else {
System.out.println("请检查输入");
}
}
}
}
/**
* 使用数组模拟环形队列----编写一个CircleQueue类
*/
class CircleQueue {
private int maxSize; // 数组最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 队列的数据
/**
* 创建队列的构造器
*/
public CircleQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[arrMaxSize];
front = 0;// 指向队列头
rear = 0;// 指向队列尾的后一个位置(空出一个空间做为约定)
}
/**
* 判断队列是否满
*/
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 + 1) % maxSize;
}
/**
* 出队列
*/
public int getQueue() {
if (isEmpty()) {
// 抛出异常 -- 不需要写return
throw new RuntimeException("队列空");
}
int value = arr[front];
front = (front + 1) % maxSize;
return value;
}
/**
* 遍历???
*/
public void showQueue() {
if (isEmpty()) {
System.out.println("队列空!");
return;
}
for (int i = front; i < front + size(); i++) {
System.out.printf("arr[%d] = %d\n", i % maxSize, arr[i % maxSize]);
}
}
/**
* 显示头数据(不取出)
*/
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空");
}
return arr[front];
}
/**
* 有效数据
*/
public int size() {
return (rear - front + maxSize) % maxSize;
}
}链表
一、单链表
1、单链表概述
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含data域, next 域:指向下一个节点.
- 链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确
2、面试题
1)有效节点个数
/**
* 有效节点个数(遍历+计数)
*/
public int usedNode() {
int ans = 0;
heroNode temp = head;
while (temp.next != null) {
ans++;
temp = temp.next;
}
return ans;
}2)倒数第K个节点数据
/**
* 反向输出第k个节点
*/
public void oppoShow(int k) {
int all = usedNode();
heroNode temp = head.next;
if (all < k) {
System.out.printf("没有第%d个节点\n", k);
return;
}
int i = 1;
while (temp != null) {
if (i == (all - k + 1)) {
System.out.println(temp.toString());
break;
}
temp = temp.next;
i++;
}
}3)反向遍历——迭代
/**
* 反向遍历
*/
public void oppoShow() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
int all = usedNode();
for (int i = all; i >= 0; i--) {
int j = 1;
heroNode temp = head.next;
while (temp != null) {
if (j == i) {
System.out.println(temp.toString());
break;
}
j++;
temp = temp.next;
}
}
}4)反向遍历——栈
class nodeStact {
int maxSize;
heroNode[] hn;
int top;
/**
* 栈的构造器
*
* @param maxSize 栈的大小
*/
nodeStact(int maxSize) {
this.maxSize = maxSize;
hn = new heroNode[maxSize];
top = -1;
}
/**
* 判空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判满
*/
public boolean isFull() {
return top == maxSize - 1;
}
/**
* 弹出
*/
public heroNode pop() {
if (isEmpty()) {
System.out.println("栈空");
return null;
}
return hn[top--];
}
/**
* 压栈
*/
public void push(heroNode _hn) {
if (isFull()) {
System.out.println("栈满");
} else {
hn[++top] = _hn;
}
}
}/**
* 反向遍历——通过栈实现
*/
public void showByStack() {
if(head.next == null) {
System.out.println("链表为空");
return;
}
nodeStact ns = new nodeStact(usedNode());
heroNode temp = head.next;
while(temp != null) {
ns.push(temp);
temp = temp.next;
}
while(!ns.isEmpty()) {
System.out.println(ns.pop().toString());
}
}5)合并单链表
/**
* 与demo合并
*/
public void mesh() {
singleLinkedList demo = new singleLinkedList();
demo.add(new heroNode(-1, "小明", "王哥"));
demo.add(new heroNode(0, "芭芭拉", "芭芭拉冲鸭"));
heroNode temp = demo.head.next;
while(temp != null) {
addByNo(new heroNode(temp.getNo(), temp.getName(), temp.getNickName()));
temp = temp.next;
}
}3、代码实现
package work.rexhao.linkedlist;
import java.util.Scanner;
public class singleLinkedListDemo {
public static void main(String[] args) {
singleLinkedList sll = new singleLinkedList();
boolean loop = true;
while (loop) {
System.out.println("--------");
System.out.println("1.在尾部添加");
System.out.println("2.按编号添加");
System.out.println("3.遍历单链表");
System.out.println("4.修改节点");
System.out.println("5.删除节点");
System.out.println("6.有效节点个数");
System.out.println("7.反向输出");
System.out.println("8.反向遍历");
System.out.println("0.退出");
System.out.println("--------");
Scanner sc = new Scanner(System.in);
int key = sc.nextInt();
if (key == 1) {
System.out.println("请输入英雄编号:");
int no = sc.nextInt();
System.out.println("请输入英雄姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
System.out.println("请输入英雄昵称:");
Scanner sc2 = new Scanner(System.in);
String nickName = sc2.nextLine();
sll.add(new heroNode(no, name, nickName));
System.out.println("添加成功!");
} else if (key == 2) {
System.out.println("请输入英雄编号:");
int no = sc.nextInt();
System.out.println("请输入英雄姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
System.out.println("请输入英雄昵称:");
Scanner sc2 = new Scanner(System.in);
String nickName = sc2.nextLine();
sll.addByNo(new heroNode(no, name, nickName));
System.out.println("添加成功!");
} else if (key == 3) {
try {
sll.show();
} catch (Exception e) {
System.out.println(e.getMessage());
}
} else if (key == 4) {
System.out.println("请输入英雄编号:");
int no = sc.nextInt();
System.out.println("请输入英雄新的姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
System.out.println("请输入英雄新的昵称:");
Scanner sc2 = new Scanner(System.in);
String nickName = sc2.nextLine();
sll.change(no, name, nickName);
} else if (key == 5) {
System.out.println("请输入英雄编号:");
int no = sc.nextInt();
sll.delete(no);
} else if (key == 6) {
System.out.println("有效节点个数:" + sll.usedNode());
} else if (key == 7) {
int k = sc.nextInt();
sll.oppoShow(k);
} else if (key == 8) {
System.out.println("通过反向遍历实现:");
sll.oppoShow();
System.out.println("通过栈实现:");
sll.showByStack();
} else if (key == 9) {
System.out.println("与demo链表合并");
sll.mesh();
} else if (key == 0) {
loop = false;
} else {
System.out.println("输入有误!");
}
}
}
}
/**
* 管理英雄
*/
class singleLinkedList {
/**
* 定义一个头结点
*/
private heroNode head = new heroNode(0, "", "");
/**
* 添加节点的方法(尾插法) 将最后的next域指向新的node
*/
public void add(heroNode hn) {
// 需要一个辅助变量
heroNode temp = head;
// 遍历单链表
while (temp.next != null) {
temp = temp.next;
}
// 退出循环时,temp指向最后
temp.next = hn;
}
/**
* 按照编号大小添加
*
* @param hn
*/
public void addByNo(heroNode hn) {
/**
* 空链表:直接放后面
*/
if (head.next == null) {
head.next = hn;
return;
}
heroNode temp = head.next;
/**
* 如果只有一个节点,且大于hn
*/
if (hn.getNo() < temp.getNo()) {
head.next = hn;
hn.next = temp;
return;
}
while (true) {
/**
* 如果no相等:报错
*/
if (hn.getNo() == temp.getNo()) {
throw new RuntimeException("编号重复!");
}
if (temp.next != null) {
if (hn.getNo() == temp.next.getNo()) {
throw new RuntimeException("编号重复!");
}
}
/**
* 找到:大于前一个节点且(小于后一个节点或者后一个节点为null)的节点
*/
if (hn.getNo() > temp.getNo() && (temp.next == null || hn.getNo() < temp.next.getNo())) {
hn.next = temp.next;
temp.next = hn;
break;
}
if (temp.next != null) {
temp = temp.next;
} else {
break;
}
}
}
/**
* 遍历
*/
public void show() {
// 判空
if (head.next == null) {
throw new RuntimeException("链表为空");
}
// 遍历
heroNode temp = head.next;
while (temp != null) {
System.out.println(temp.toString());
temp = temp.next;
}
}
/**
* 修改节点
*/
public void change(int no, String name, String nickName) {
// 判空
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 遍历:找no节点
heroNode temp = head.next;
while (temp != null) {
if (temp.getNo() == no) {
temp.setName(name);
temp.setNickName(nickName);
System.out.println("修改成功!");
return;
}
temp = temp.next;
}
System.out.printf("未找到编号为%d的节点\n", no);
return;
}
/**
* 删除节点
*/
public void delete(int no) {
// 判空
if (head.next == null) {
System.out.println("链表为空");
return;
}
// 遍历找节点
heroNode temp = head;
while (temp.next != null) {
if (temp.next.getNo() == no) {
temp.next = temp.next.next;
System.out.println("删除成功");
return;
}
temp = temp.next;
}
System.out.printf("未找到编号为%d的节点\n", no);
return;
}
/**
* 有效节点个数(遍历+计数)
*/
public int usedNode() {
int ans = 0;
heroNode temp = head;
while (temp.next != null) {
ans++;
temp = temp.next;
}
return ans;
}
/**
* 反向输出第k个节点
*/
public void oppoShow(int k) {
int all = usedNode();
heroNode temp = head.next;
if (all < k) {
System.out.printf("没有第%d个节点\n", k);
return;
}
int i = 1;
while (temp != null) {
if (i == (all - k + 1)) {
System.out.println(temp.toString());
break;
}
temp = temp.next;
i++;
}
return;
}
/**
* 反向遍历——翻转单链表
*/
public void oppoShow() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
int all = usedNode();
for (int i = all; i >= 0; i--) {
int j = 1;
heroNode temp = head.next;
while (temp != null) {
if (j == i) {
System.out.println(temp.toString());
break;
}
j++;
temp = temp.next;
}
}
}
/**
* 反向遍历——通过栈实现
*/
public void showByStack() {
if (head.next == null) {
System.out.println("链表为空");
return;
}
nodeStact ns = new nodeStact(usedNode());
heroNode temp = head.next;
while (temp != null) {
ns.push(temp);
temp = temp.next;
}
while (!ns.isEmpty()) {
System.out.println(ns.pop().toString());
}
}
/**
* 与demo合并
*/
public void mesh() {
singleLinkedList demo = new singleLinkedList();
demo.add(new heroNode(-1, "小明", "王哥"));
demo.add(new heroNode(0, "芭芭拉", "芭芭拉冲鸭"));
heroNode temp = demo.head.next;
while(temp != null) {
addByNo(new heroNode(temp.getNo(), temp.getName(), temp.getNickName()));
temp = temp.next;
}
}
}
/**
* 定义heroNode节点
*/
class heroNode {
private int no;
private String name;
private String nickName;
public heroNode next;
/**
* 构造器
*
* @param no 编号
* @param name 姓名
* @param nickName 昵称
*/
heroNode(int no, String name, String nickName) {
this.name = name;
this.no = no;
this.nickName = nickName;
}
/**
* 重写toString
*/
@Override
public String toString() {
return "[no=" + no + ", name=" + name + ", nickName=" + nickName + ", next=" + next + "]";
}
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;
}
}
class nodeStact {
int maxSize;
heroNode[] hn;
int top;
/**
* 栈的构造器
*
* @param maxSize 栈的大小
*/
nodeStact(int maxSize) {
this.maxSize = maxSize;
hn = new heroNode[maxSize];
top = -1;
}
/**
* 判空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判满
*/
public boolean isFull() {
return top == maxSize - 1;
}
/**
* 弹出
*/
public heroNode pop() {
if (isEmpty()) {
System.out.println("栈空");
return null;
}
return hn[top--];
}
/**
* 压栈
*/
public void push(heroNode _hn) {
if (isFull()) {
System.out.println("栈满");
} else {
hn[++top] = _hn;
}
}
}二、双向链表
1、双向链表概述
- 遍历方法和单链表一样,不仅可以向前,也可以向后查找
- 添加(默认添加到双向链表的最后)
- 先找到双向链表的最后这个节点
- temp.next = newHeroNode
- newHeroNode.pre=temp;
- 修改:思路和原来的单向链表一样
- 删除
- 双向链表可以实现自我删除某个节点
- 直接找到要州除的这个节点,比如 temp
- temp.pre.next =temp.next
- temp.next. pre = temp.pre;
2、代码实现
package work.rexhao.linkedlist;
import java.util.Scanner;
public class doubleLinkedListDemo {
public static void main(String[] args) {
doubleLinkedList dll = new doubleLinkedList();
boolean loop = true;
while (loop) {
System.out.println("--------");
System.out.println("1.在尾部添加");
System.out.println("2.按编号添加");
System.out.println("3.遍历双向链表");
System.out.println("4.修改节点");
System.out.println("5.删除节点");
System.out.println("6.有效节点个数");
System.out.println("0.退出");
System.out.println("--------");
Scanner sc = new Scanner(System.in);
int key = sc.nextInt();
if (key == 1) {
System.out.println("请输入学生编号:");
int no = sc.nextInt();
System.out.println("请输入学生姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
dll.add(new stuNode(no, name));
System.out.println("添加成功!");
} else if (key == 2) {
System.out.println("请输入学生编号:");
int no = sc.nextInt();
System.out.println("请输入学生姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
dll.addByNo(new stuNode(no, name));
System.out.println("添加成功!");
} else if (key == 3) {
dll.show();
} else if (key == 4) {
System.out.println("请输入学生编号:");
int no = sc.nextInt();
System.out.println("请输入学生新的姓名:");
Scanner sc1 = new Scanner(System.in);
String name = sc1.nextLine();
dll.change(new stuNode(no, name));
} else if (key == 5) {
System.out.println("请输入学生编号:");
int no = sc.nextInt();
dll.delete(no);
} else if (key == 6) {
System.out.println("有效节点个数:" + dll.usedNode());
} else if (key == 0) {
loop = false;
} else {
System.out.println("输入有误!");
}
}
}
}
/**
* 双向链表
*/
class doubleLinkedList {
private stuNode head = new stuNode(0, "wmh");
/**
* 有效节点数
*/
public int usedNode() {
int ans = 0;
stuNode temp = head.next;
while (temp != null) {
ans++;
temp = temp.next;
}
return ans;
}
/**
* 插入
*/
public void add(stuNode sn) {
if (head.next == null) {
head.next = sn;
sn.pre = head;
return;
}
stuNode temp = head.next;
while (true) {
if (temp.next == null) {
break;
}
temp = temp.next;
}
temp.next = sn;
sn.pre = temp;
}
/**
* 遍历
*/
public void show() {
if (head.next == null) {
System.out.println("单链表为空");
return;
}
stuNode temp = head.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
/**
* 修改
*/
public void change(stuNode sn) {
if (head.next == null) {
System.out.println("单链表为空");
return;
}
stuNode temp = head.next;
while (temp != null) {
if (temp.getNo() == sn.getNo()) {
temp.setName(sn.getName());
temp.setNo(sn.getNo());
return;
}
temp = temp.next;
}
System.out.printf("未找到no=%d的节点\n", sn.getNo());
}
/**
* 自我删除
*/
public void delete(int no) {
if (head.next == null) {
System.out.println("链表为空");
return;
}
stuNode temp = head.next;
while (temp != null) {
if (temp.getNo() == no) {
/*
* 找到了
*/
temp.pre.next = temp.next;
if (temp.next != null) {
temp.next.pre = temp.pre;
}
return;
}
temp = temp.next;
}
System.out.printf("未找到no=%d的节点\n", no);
}
/**
* 按照编号添加
*/
public void addByNo(stuNode sn) {
/*
* 判空
*/
if (head.next == null) {
head.next = sn;
sn.pre = head;
return;
}
/*
* 判同
*/
stuNode temp = head.next;
while (temp != null) {
if (temp.getNo() == sn.getNo()) {
System.out.println("编号相同");
return;
}
temp = temp.next;
}
/*
* 找位置添加——第一个位置
*/
if (sn.getNo() < head.next.getNo()) {
sn.next = head.next;
head.next = sn;
sn.pre = head;
sn.next.pre = sn;
return;
}
/*
* 找位置添加——不是第一个位置
*/
temp = head.next;
while (temp != null) {
if (temp.next == null) {
temp.next = sn;
sn.pre = temp;
return;
}
if (temp.getNo() < sn.getNo() && temp.next.getNo() > sn.getNo()) {
sn.next = temp.next;
temp.next = sn;
sn.pre = head;
sn.next.pre = sn;
return;
}
temp = temp.next;
}
}
}
/**
* 定义stuNode节点
*/
class stuNode {
private int no;
private String name;
public stuNode next;
public stuNode pre;
/**
* 构造器
*
* @param no 编号
* @param name 姓名
*/
stuNode(int no, String name) {
this.name = name;
this.no = no;
}
public int getNo() {
return no;
}
/**
* 重写toString
*/
@Override
public String toString() {
return "[no=" + no + ", name=" + name + "]";
// 使用双向链表,不能打印next和pre
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}三、单项循环链表与Josepfu问题
1、约瑟夫环概述
设编号为1,2,⋯n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
2、解决思想
用一个不带头结点的循环链表来处理 Josephu 问题:先构成一个有口个结点的单循环链表,然后由k结点起从1 开始计数,计到 m 时,对应结点从链表中州除,然后再从被删除结点的下一个结点又从1 开始计数,到最后一个结点从链表中删除算法结束。
3、代码实现
package work.rexhao.linkedlist;
import java.util.Scanner;
public class JosepfuDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("总人数:");
int n = sc.nextInt();
System.out.print("报号数:");
int k = sc.nextInt();
Josepfu(n, k);
}
/**
* Josepfu问题
*
* @param n 人数
* @param k 报号数
*/
public static void Josepfu(int n, int k) {
circleSingleLinkedList csll = new circleSingleLinkedList();
for(int i = 1; i <= n; i++) {
csll.add(new Node(i));
}
Node temp = csll.head;
int j = 1;
while(csll.usedNode() != 1) {
if(j == k) {
csll.delete(temp.getNum());
j = 1;
temp = temp.next;
continue;
}
j++;
temp = temp.next;
}
csll.show();
}
}
class circleSingleLinkedList {
public Node head = null;
/**
* 添加节点
*/
public void add(Node n) {
/*
* 空链表,直接放后面
*/
if (head == null) {
head = n;
n.next = head;
return;
}
/*
* 非空,遍历加在后面
*/
Node temp = head;
while (temp.next != head) {
temp = temp.next;
}
temp.next = n;
n.next = head;
}
/**
* 遍历
*/
public void show() {
Node temp = head;
boolean loop = true;
while (loop) {
System.out.println(temp);
temp = temp.next;
if (temp == head) {
loop = false;
}
}
}
/**
* 有效元素
*/
public int usedNode() {
if (head == null) {
return 0;
}
if (head.next == head) {
return 1;
}
Node temp = head;
int ans = 1;
while (temp.next != head) {
temp = temp.next;
ans++;
}
return ans;
}
/**
* 删除节点
*/
public void delete(int num) {
/*
* 判空
*/
if (head == null) {
System.out.println("链表为空");
return;
}
/*
* 删除head节点
*/
if (head.getNum() == num) {
if (head.next == head) {
/*
* 只有一个head
*/
head = null;
} else {
/*
* 还有其他节点
*/
// 1.改最后的next
Node temp = head;
while (true) {
if (temp.next == head) {
temp.next = head.next;
break;
}
temp = temp.next;
}
// 2.改head
head = head.next;
}
return;
}
/*
* 删除其他的节点
*/
Node temp = head.next;
while (true) {
if (temp.next.getNum() == num) {
temp.next = temp.next.next;
return;
}
temp = temp.next;
}
}
}
class Node {
private int num;
public Node next;
Node(int num, Node next) {
this.num = num;
this.next = next;
}
Node(int num) {
this.num = num;
this.next = null;
}
@Override
public String toString() {
return "Node[num=" + num + "]";
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}栈
一、栈的概述
1、栈的介绍
- 栈的英文为stack
- 栈是一个先入后出(FILO-First In Last Out)的有序列表。
- 栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。
- 允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)
- 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除
2、栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。
- 表达式的转换(中缀表达式转后级表达式)与求值(实际解决)。
- 二叉树的遍历。
- 图的深度优先搜索dfs。
3、栈的代码实现
1)数组实现
package work.rexhao.stack;
public class arrayStackdemo {
public static void main(String[] args) {
arrayStact as = new arrayStact(10);
as.push(1);
as.push(2);
as.list();
System.out.println(as.pop());
System.out.println(as.pop());
as.list();
}
}
class arrayStact {
int maxSize;
int[] data;
int top;
/**
* 栈的构造器
*
* @param maxSize 栈的大小
*/
arrayStact(int maxSize) {
this.maxSize = maxSize;
data = new int[maxSize];
top = -1;
}
/**
* 判空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判满
*/
public boolean isFull() {
return top == maxSize - 1;
}
/**
* 弹出
*/
public int pop() {
if (isEmpty()) {
System.out.println("栈空");
return -1;
}
return data[top--];
}
/**
* 压栈
*/
public void push(int num) {
if (isFull()) {
System.out.println("栈满");
} else {
data[++top] = num;
}
}
/**
* 遍历
*/
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = 0; i <= top; i++) {
System.out.printf("data[%d] = %d\n", i, data[i]);
}
}
}2)链表实现
package work.rexhao.stack;
public class linkedListStackDemo {
public static void main(String[] args) {
linkedListStack lls = new linkedListStack();
try {
lls.pop();
} catch (Exception e) {
System.out.println(e.getMessage());
}
lls.push(1);
lls.push(2);
lls.pop();
lls.push(3);
lls.push(4);
lls.show();
}
}
class linkedListStack {
private stackNode head = new stackNode(-1);
/**
* 压栈
*/
public void push(int num) {
if (head.next == null) {
head.next = new stackNode(num);
return;
}
stackNode temp = head.next;
while (true) {
if (temp.next == null) {
temp.next = new stackNode(num);
return;
}
temp = temp.next;
}
}
/**
* 出栈
*/
public int pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
stackNode temp = head;
while (true) {
if (temp.next.next == null) {
break;
}
temp = temp.next;
}
int ans = temp.next.getData();
temp.next = null;
return ans;
}
/**
* 判空
*/
public boolean isEmpty() {
return head.next == null;
}
/**
* 遍历
*/
public void show() {
if (isEmpty()) {
return;
}
stackNode temp = head.next;
while (true) {
if (temp == null) {
return;
}
System.out.println(temp.toString());
temp = temp.next;
}
}
}
class stackNode {
private int data;
public stackNode next;
stackNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
@Override
public String toString() {
return data + "";
}
}二、栈实现综合计算器
1、思路
- 通过一个index 值(索引),来遍历我们的表达式
- 扫描到一个数字,直接入数栈
- 扫描到一个符号
- 符号栈为空,就直接入栈
- 有操作符,就进行比较
- 如果当前的操作符的优先级小于或者等于栈顶的操作符,就需要从数栈中pop出两个数,在从符号栈中pop出一个符号,进行运算,将得到结果入数栈,然后将当前的操作符入符号栈
- 如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈
- 当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号
- 最后在数栈只有一个数字,就是表达式的结果
2、代码实现
package work.rexhao.stack;
import java.util.Scanner;
/**
* 个位数四则计算器
*
*/
public class calculatorDemo {
public static void main(String[] args) {
System.out.println("请输入表达式");
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
sc.close();
System.out.println("表达式值为:" + calculator.calculate(s));
}
}
class calculator {
/**
* 计算
*
* @param s 表达式
* @return 结果
*/
public static int calculate(String s) {
/*
* 初始化栈
*/
numStact ns = new numStact(20); // 数栈
signStact ss = new signStact(20); // 运算符栈
/*
* 扫描字符串
*/
int slen = s.length();
// int index = 0; // 扫描索引
for (int i = 0; i < slen; i++) {
char c = s.charAt(i);
if (c == '*' || c == '/') {
ss.push(c);
} else if (c == '+' || c == '-') {
while (true) {
// 栈空:入栈
if (ss.isEmpty()) {
break;
}
// 栈非空:比较
if (ss.top() == '+' || ss.top() == '-') {
// 同优先级
break;
} else {
// 大于优先级
// 1.取出两个数和运算符
int ans = count(ns.pop(), ns.pop(), ss.pop());
// 2.结果入栈
ns.push(ans);
}
}
ss.push(c);
} else {
ns.push(c - '0');
}
}
while(ns.top != 0) {
ns.push(count(ns.pop(), ns.pop(), ss.pop()));
}
return ns.pop();
}
/**
* 辅助计算
*/
private static int count(int a, int b, char c) {
if (c == '+') {
return a + b;
} else if (c == '-') {
return b - a;
} else if (c == '*') {
return a * b;
} else if (c == '/') {
return a / b;
}
throw new RuntimeException("NaN");
}
}
/**
* 数栈
*
*/
class numStact {
int maxSize;
int[] data;
int top;
/**
* 栈的构造器
*
* @param maxSize 栈的大小
*/
numStact(int maxSize) {
this.maxSize = maxSize;
data = new int[maxSize];
top = -1;
}
/**
* 判空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判满
*/
public boolean isFull() {
return top == maxSize - 1;
}
/**
* 弹出
*/
public int pop() {
if (isEmpty()) {
System.out.println("栈空");
return -1;
}
return data[top--];
}
/**
* 压栈
*/
public void push(int num) {
if (isFull()) {
System.out.println("栈满");
} else {
data[++top] = num;
}
}
/**
* 遍历
*/
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = 0; i <= top; i++) {
System.out.printf("data[%d] = %d\n", i, data[i]);
}
}
}
/**
* 符号栈
*
*/
class signStact {
int maxSize;
char[] data;
int top;
/**
* 栈的构造器
*
* @param maxSize 栈的大小
*/
signStact(int maxSize) {
this.maxSize = maxSize;
data = new char[maxSize];
top = -1;
}
/**
* 判空
*/
public boolean isEmpty() {
return top == -1;
}
/**
* 判满
*/
public boolean isFull() {
return top == maxSize - 1;
}
/**
* 弹出
*/
public char pop() {
if (isEmpty()) {
throw new RuntimeException("栈空");
}
return data[top--];
}
/**
* 压栈
*/
public void push(char c) {
if (isFull()) {
System.out.println("栈满");
} else {
data[++top] = c;
}
}
/**
* 遍历
*/
public void list() {
if (isEmpty()) {
System.out.println("栈空");
return;
}
for (int i = 0; i <= top; i++) {
System.out.printf("data[%d] = %s\n", i, data[i]);
}
}
/**
* 栈顶元素
*/
public char top() {
return data[top];
}
}三、逆波兰计算器
package work.rexhao.stack;
public class polandNotationDemo {
public static void main(String[] args) {
System.out.println(polandNotation.calculate("34+5*6-"));
}
}
class polandNotation {
public static int calculate(String s) {
numStact ns = new numStact(20);
// 不需要符号栈
int slen = s.length();
for (int i = 0; i < slen; i++) {
char c = s.charAt(i);
if (c == '*' || c == '/' || c == '+' || c == '-') {
ns.push(count(ns.pop(), ns.pop(), c));
} else {
ns.push(c - '0');
}
}
return ns.pop();
}
/**
* 辅助计算
*/
private static int count(int a, int b, char c) {
if (c == '+') {
return a + b;
} else if (c == '-') {
return b - a;
} else if (c == '*') {
return a * b;
} else if (c == '/') {
return a / b;
}
throw new RuntimeException("NaN");
}
}四、中缀转后缀
1、思路
后缀表达式适合计算式进行运算,但是却不太容易写出来,尤其是表达式很长的情况下
因此在开发中,我们常常需要将中缀表达式转成后缀表达式
- 初始化两个栈:运算符栈s1和储存中间结果的栈s2;
- 从左至右扫描中缀表达式
- 遇到操作数时,将其压s2
- 遇到运算符时,比较其与 s1栈顶运算符的优先级
- 如果s1为空,或栈顶运算符为左括号
(,压入s1 - 若该运算符优先级比栈顶运算符的高,压入s1
- 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到与与s1中新的栈顶运算符相比较
- 如果s1为空,或栈顶运算符为左括号
- 遇到括号
- 如果是左括号
(,则直接压入s1 - 如果是右括号
),则依次弹出s1的运算符压s2,直到遇到左括号为止,将这一对括号丢弃
- 如果是左括号
- 重复步骤直到表达式的最右边
- 将s1中剩余的运算符依次弹出并压入s2
- 依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
2、代码实现
/**
* 中缀转后缀
*
* @param s 中缀表达式
* @return 后缀表达式
*/
public static String toPoland(String s) {
sStact s1 = new sStact(20);// 运算符栈
sStact s2 = new sStact(20);// 中间结果栈
int slen = s.length();
for (int i = 0; i < slen; i++) {
char c = s.charAt(i);
if (s1.isEmpty() || c == '(') {
s1.push(c);
} else if (c == ')') {
while (true) {
char temp = s1.pop();
if (temp == '(') {
break;
}
s2.push(temp);
}
} else if (c == '+' || c == '-') {
while (true) {
if (s1.isEmpty() || s1.top() == '(') {
s1.push(c);
break;
}
s2.push(s1.pop());
}
} else if (c == '*' || c == '/') {
while (true) {
if (s1.isEmpty() || s1.top() == '+' || s1.top() == '-' || s1.top() == '(') {
s1.push(c);
break;
}
s2.push(s1.pop());
}
} else {
// 数字
s2.push(c);
}
}
while (!s1.isEmpty()) {
s2.push(s1.pop());
}
StringBuilder sb = new StringBuilder();
while (!s2.isEmpty()) {
sb.append(s2.pop());
}
return sb.reverse().toString();
}递归
一、递归概述
递归:方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
二、递归的调用机制
- 当程序执行到一个方法时,就会开辟一个独立的空间(栈)
- 每个空间的数据(局部变量)都是独立的
三、递归解决的问题
- 各种数学问题如:8 皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题(google 编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
- 将用栈解决的问题 --> 递归代码比较简洁
四、递归的原则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
- 方法的局部变量是独立的,不会相互影响,比如n变量
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完华。
五、迷宫问题
package work.rexhao.recursion;
import java.util.Scanner;
public class mazeDemo {
static int size;
static String[] map;
static int ans = 0;
static boolean[][] flag;
public static void main(String[] args) {
System.out.println("输入地图的大小:");
Scanner sc = new Scanner(System.in);
size = Integer.parseInt(sc.nextLine());
System.out.println("输入地图:");
map = new String[size];
flag = new boolean[size][size];
for (int i = 0; i < size; i++) {
map[i] = sc.nextLine();
}
int x = 0, y = 0;
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
if (map[i].charAt(j) == 'B') {
x = i;
y = j;
}
}
}
maze(x, y);
System.out.println(ans);
sc.close();
}
public static void maze(int x, int y) {
if (check(x, y) && map[x].charAt(y) == 'E') {
ans++;
return;
}
flag[x][y] = true;
if (check(x + 1, y)) {
maze(x + 1, y);
}
if (check(x - 1, y)) {
maze(x - 1, y);
}
if (check(x, y + 1)) {
maze(x, y + 1);
}
if (check(x, y - 1)) {
maze(x, y - 1);
}
flag[x][y] = false;
}
public static boolean check(int x, int y) {
if (x >= size || y >= size || x < 0 || y < 0) {
return false;
} else
return (map[x].charAt(y) == '.' || map[x].charAt(y) == 'E') && !flag[x][y];
}
}六、回溯算法(八皇后问题)
1、八皇后问题介绍
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯 • 贝瑟尔于1848 年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即:任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法(92)。
2、思路分析
- 相同剪枝:如果在同一行/列,舍弃
- 斜向剪枝:对于y=x方向,
行+列为定值;对于y=-x方向,行-列为定值
3、代码实现
package work.rexhao.recursion;
public class queueEight {
public static int[] chess = new int[8];
public static int ans = 0;
public static int[] x1; // 对角线1
public static int[] x2; // 对角线2
public static void main(String[] args) {
queue(chess, 0);
System.out.println(ans);
}
public static void queue(int[] chess, int times) {
/*
* 退出条件
*/
if (times == 8) {
/*
* 重复
*/
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
if(i == j) continue;
if (chess[i] == chess[j]) {
return;
}
}
}
/*
* 对角线
*/
x1 = new int[20];
x2 = new int[20];
for (int i = 0; i < 8; i++) {
x1[chess[i] - i + 8]++;
x2[i + chess[i]]++;
}
for (int i = 0; i < 20; i++) {
if (x1[i] > 1 || x2[i] > 1) {
return;
}
}
for(int i: chess) {
System.out.print(i + " ");
}
System.out.println();
ans++;
return;
}
/*
* 递归
*/
for (int i = 1; i < 9; i++) {
chess[times] = i;
queue(chess, times + 1);
}
}
}排序算法
一、排序算法概述
1、排序的介绍
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程
2、排序的分类
1)存储介质
内部排序:数据量不大,数据在内存
外部排序:数据量大,数据在外存,分批调入内存
2)比较器个数
串行排序:单处理机
并行排序:多处理机
3)主要操作
比较排序:用比较的方法
基数排序:根据取值确定有序位置
4)辅助空间
原地排序:空间O(1),无辅助空间
非原地排序:空间不是O(1)
5)稳定性
稳定性只对结构类型数据有意义
稳定排序:相等元素排序后次序不变
非稳定排序:相等元素排序后次序发生了变化
6、自然性
自然排序:原来的数据有序,排序速度很快
非自然排序:原来的数据有序,但排序速度却变慢了
二、算法的复杂度
1、时间度量方法
- 事后统计法
- 事前估计法
2、计算时间复杂度
- 用常数1代替运行时间中的所有加法常数
- 修改后的运行次数两数中,只保留最高阶项
- 去除最高阶项的系数
3、常见的时间复杂度
- 常数阶 O(1)
- 对数阶 O(log2n)
- 线性阶 O(n)
- 线性对数阶 O(nlog2n)
- 平方阶 O(n^2)
- 立方阶 O(n^3)
- k次方阶 O(n^k)
- 指数阶 O(2^n)
4、平均时间复杂度和最坏时间复杂度
平均时问复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。
平均时间复杂度和最坏时间复杂度是否一致,和算法有关
5、算法的空间复杂度
类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模口有关,它随着口的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法,基数排序就属于这种情况
在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis、memcache)和算法(基数排序)本质就是用空间换时间
三、排序算法
1、冒泡排序
1)介绍
冒泡排序(Bubble Sorting):通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
2)优化
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 冒泡排序
* @date 2022/7/2 10:25
*/
public class BubbleSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
for(int i = 0; i < num.length; i++){
boolean flag = true;
for (int j = 0; j < num.length - i - 1; j++) {
if(num[j] > num[j+1]){
// 交换
int temp = num[j];
num[j] = num[j+1];
num[j+1] = temp;
// 标记
flag = false;
}
}
if(flag){
break;
}
}
System.out.println(Arrays.toString(num));
}
}2、选择排序
1)介绍
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
2)思想
后面序列中找到最值与前面元素互换
- 选择排序一共有“数组大小 - 1”轮排序
- 每轮排序,又是一个循环,循环的规则
- 先假定当前这个数是最小数
- 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
- 当遍历到数组的最后时,就得到本轮最小数和下标
- 交换
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 选择排序
* @date 2022/7/2 10:24
*/
public class SelectSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
for (int i = 0; i < num.length; i++) {
// 找最小值
int min = i;
for (int j = i + 1; j < num.length; j++) {
min = num[j] > num[min] ? min :j;
}
// 交换
int temp = num[i];
num[i] = num[min];
num[min] = temp;
}
System.out.println(Arrays.toString(num));
}
}3、插入排序
1)介绍
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的
2)思想
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 插入排序
* @Date 2022/7/2 10:35
*/
public class InsertSort {
public static void main(String[] args) {
int[] num = new int[]{10,9,8,7,6,5,4,3,2,1};
System.out.println(Arrays.toString(num));
for (int i = 1; i < num.length; i++) {
// 取出无需表首元素
int temp = num[i];
// 把temp找位置插入
for (int j = i ; j >= 0; j--) {
// 找到大于前一个元素的位置插入
// 如果是最小值,j == 0,插入最前面
if(temp > num[j] || j == 0){
num[j] = temp;
break;
}
// 元素后移(对于插入元素已经存在temp中了)
num[j] = num[j - 1];
}
}
System.out.println(Arrays.toString(num));
}
}4、希尔排序
1)介绍
希尔排序是希尔 (Donald Shell)于 1959 年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
2)基本思想
插入排序的问题:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响.
把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 希尔排序
* @Date 2022/7/3 08:51
*/
public class ShellSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
shell_2(num);
System.out.println(Arrays.toString(num));
}
/**
* 希尔排序_交换法
*
* @param num 待排序数组
*/
public static void shell_1(int[] num) {
// 分组
// gap:每组的元素个数
for (int gap = num.length / 2; gap > 0; gap /= 2) {
// 从最后一组的第一个元素开始,依次与上一组的对应元素比较并交换
for (int i = num.length - gap - 1; i < num.length; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (num[j] > num[j + gap]) {
int temp = num[j];
num[j] = num[j + gap];
num[j + gap] = temp;
}
}
}
}
}
/**
* 希尔排序_移位法
*
* @param num 待排序数组
*/
public static void shell_2(int[] num) {
int count = 0;
// 分组
// gap:每组的元素个数
for (int gap = num.length / 2; gap > 0; gap /= 2) {
// 从第二组开始
for (int i = gap; i < num.length; i++) {
// 记录被操作的值
int temp = num[i];
// 移动前面元素
for (int j = i; j >= 0; j -= gap) {
// 找到前一个位置比自己小的,否则向后移动元素
if (j - gap < 0 || num[j - gap] < temp) {
num[j] = temp;
break;
} else {
num[j] = num[j - gap];
}
}
}
}
}
}5、快速排序
1)介绍
快速排序(Quicksort)是对冒泡排序的一种改进算法
2)基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 快速排序
* @Date 2022/7/3 11:48
*/
public class QuickSort {
public static void main(String[] args) {
int[] num = new int[]{10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
System.out.println(Arrays.toString(num));
quickSort(num, 0, num.length - 1);
System.out.println(Arrays.toString(num));
}
private static void quickSort(int[] num, int left, int right) {
if (left >= right) {
return;
}
int l = left;
int r = right;
int pivot = num[(l + r) / 2];
while (l < r) {
while (num[l] < pivot) {
l++;
}
while (num[r] > pivot) {
r--;
}
if (l > r) {
break;
}
int temp = num[l];
num[l] = num[r];
num[r] = temp;
l++;
r--;
}
quickSort(num, 0, r);
quickSort(num, l, right);
}
}6、归并排序
1)介绍
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略
分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
2)思想
3)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 归并排序
* @Date 2022/7/3 22:54
*/
public class MergeSort {
public static void main(String[] args) {
int[] num = new int[]{12, 32, 45, 64, 83, 64, 23, 65, 10};
System.out.println(Arrays.toString(num));
int[] temp = new int[num.length];
mergeSort(num, temp, 0, num.length - 1);
System.out.println(Arrays.toString(num));
}
public static void mergeSort(int[] num, int[] temp, int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
mergeSort(num, temp, left, mid);
mergeSort(num, temp, mid + 1, right);
merge(num, temp, left, mid, right);
}
}
public static void merge(int[] num, int[] temp, int left, int middle, int right) {
int i = left;
int j = middle + 1;
int t = 0;
// 1.把左右有序放在temp
while (i <= middle && j <= right) {
if (num[i] <= num[j]) {
temp[t++] = num[i++];
} else {
temp[t++] = num[j++];
}
}
// 2.把剩余的一方放到temp
while (i <= middle) {
temp[t++] = num[i++];
}
while (j <= right) {
temp[t++] = num[j++];
}
// 3.把temp复制到num
t = 0;
for (int ii = left; ii <= right; ii++) {
num[ii] = temp[t++];
}
}
}7、基数排序(桶排序)
1)介绍
- 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort / bin sort)。它是通过键值的各个位的值,将要排序的元素分配至某些 “桶”中,达到排序的作用
- 基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
- 基数排序(Radix Sort)是桶排序的扩展
- 基数排序是 1887年赫尔曼 • 何乐礼发明的。将整数按位数切割成不同的数字,然后按每个位数分别比较
2)思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。
这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
3)注意事项
- 基数排序是对传统桶排序的扩展,速度很快.
- 基数排序是经典的空间换时间的方式,占用内存很大,当对海量数据排序时,容易造成 OutOfMemory Error。
- 基数排序时稳定的。
- 数组不能有负数
4)代码实现
package work.rexhao.sort;
import java.util.Arrays;
/**
* 基数排序
* @Date 2022/7/3 22:22
*/
public class RadixSort {
public static void main(String[] args) {
int[] num = new int[]{12,32,45,64,83,64,23,65,10};
System.out.println(Arrays.toString(num));
radixSort(num);
System.out.println(Arrays.toString(num));
}
private static void radixSort(int[] num) {
// 对个位
ArrayQueue aq[] = new ArrayQueue[10]; // 对象数组
for (int i = 0; i < 10; i++) {
aq[i] = new ArrayQueue(num.length);
}
for (int i = 0; i < num.length; i++) {
aq[num[i] % 10].addQueue(num[i]);
}
int count = 0;
for (int i = 0; i < 10; i++) {
while(!aq[i].isEmpty()){
num[count++] = aq[i].getQueue();
}
}
// 对十位
for (int i = 0; i < num.length; i++) {
aq[num[i] / 10].addQueue(num[i]);
}
count = 0;
for (int i = 0; i < 10; i++) {
while(!aq[i].isEmpty()){
num[count++] = aq[i].getQueue();
}
}
}
}
/**
* 使用数组模拟队列----编写一个ArrayQueue类
*/
class ArrayQueue {
private int maxSize; // 数组最大容量
private int front; // 队列头
private int rear; // 队列尾
private int[] arr; // 队列的数据
/**
* 创建队列的构造器
*/
public ArrayQueue(int arrMaxSize) {
maxSize = arrMaxSize;
arr = new int[arrMaxSize];
front = -1;// 指向队列头的前一个位置
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;
}
arr[++rear] = n;
}
/**
* 出队列
*/
public int getQueue() {
if (isEmpty()) {
// 抛出异常 -- 不需要写return
throw new RuntimeException("队列空");
}
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\n", i, arr[i]);
}
}
/**
* 显示头数据(不取出)
*/
public int headQueue() {
if (isEmpty()) {
throw new RuntimeException("队列空");
}
return arr[front + 1];
}
}四、常用排序总结对比
| 排序方法 | 时间-平均 | 时间-最好 | 时间-最坏 | 空间 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | n^2 | n | n^2 | 1 | 稳定 |
| 选择排序 | n^2 | n^2 | n^2 | 1 | 不稳定 |
| 插入排序 | n^2 | n | n^2 | 1 | 稳定 |
| 希尔排序 | nlogn | nlog^2 2 | nlog^2 2 | 1 | 不稳定 |
| 归并排序 | nlogn | nlogn | nlogn | n | 稳定 |
| 快速排序 | nlogn | nlogn | n^2 | logn | 不稳定 |
| 堆排序 | nlogn | nlogn | nlogn | 1 | 不稳定 |
| 基数排序 | n * k | n * k | n * k | n + k | 稳定 |
查找算法
一、线性查找
package work.rexhao.search;
public class SeqSearch {
public static void main(String[] args) {
int[] num = new int[]{12, 32, 45, 69, 83, 64, 23, 65, 10};
System.out.println(seqSearch(num, 83));
System.out.println(seqSearch(num, 99));
}
private static int seqSearch(int[] num, int i) {
for (int j = 0; j < num.length; j++) {
if (num[j] == i) {
return j;
}
}
return -1;
}
}二、二分查找
1、思路
- 确定该数组的中间的下标:mid = (left +right) / 2
- 然后让需要查找的数findVal和 arr[mid] 比较:
- findVal > arr[mid],说明你要查找的数在mid 的右边,因此需要递归的向右查找
- findVal < arr[mid],说明你要查找的数在mid 的左边,因此需要递归的向左查找
- findval == arr[mid],说明找到,就返回
- 什么时候我们需要结束递归?
- 找到就结束递归
- 递归完整个数组,仍然没有找到findVal,也需要结束递归(当left>right就要退出)
2、代码实现
package work.rexhao.search;
import java.util.Arrays;
/**
* 二分查找
* @Date 2022/7/3 22:54
*/
public class BinarySearch {
public static void main(String[] args) {
int[] num = new int[]{10, 12, 23, 32, 45, 64, 65, 69, 83};
System.out.println(Arrays.toString(num));
System.out.println(binarySearch(num, 69, 0, num.length - 1));
System.out.println(binarySearch(num, 99, 0, num.length - 1));
}
public static int binarySearch(int[] num, int target, int left, int right) {
if (left > right) {
return -1;
}
if (num[(left + right) / 2] == target) {
return (left + right) / 2;
} else if (num[(left + right) / 2] > target) {
return binarySearch(num, target, left, (left + right) / 2);
} else {
return binarySearch(num, target, (left + right) / 2 + 1, right);
}
}
}三、差值查找
1、思路
插值查找算法类似于二分查找,不同的是插值查找每次从自适应 mid 处开始查找。
自适应mid算法:int mid = left + (right - left) * (target - num[left]) / (num[right] - num[left]);
对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快.
关键字分布不均匀的情况下,该方法不一定比折半查找要好
2、代码实现
package work.rexhao.search;
import java.util.Arrays;
/**
* 差值查找
* @Date 2022/7/4 10:54
*/
public class InsertValueSearch {
public static void main(String[] args) {
int[] num = new int[]{10, 12, 23, 32, 45, 64, 65, 69, 83};
System.out.println(Arrays.toString(num));
System.out.println(insertValueSearch(num, 69, 0, num.length - 1));
System.out.println(insertValueSearch(num, 99, 0, num.length - 1));
}
private static int insertValueSearch(int[] num, int target, int left, int right) {
int mid = left + (right - left) * (target - num[left]) / (num[right] - num[left]);
if (left > right || mid >= num.length || mid < 0) {
return -1;
}
if (num[mid] == target) {
return mid;
} else if (num[mid] > target) {
return insertValueSearch(num, target, left, mid);
} else {
return insertValueSearch(num, target, mid + 1, right);
}
}
}四、斐波那契查找
1、思路
mid 位于黄金分割点附近,即 mid-low+F(k-1)-1(F 代表斐波那契数列)
由斐波那契数列 F[k]=F[K-1]+F[k-2]的性质,可以得到 (F[k]-1)=(F[k-1]-1)+(F[k-2]-1)+1
(F[k-1]-1):左边的子序列 (F[k-2]-1):右边子序列 1:mid
每一子段也用相同的方式分割
顺序表长度n不一定刚好等于 F[k]-1,所以需要将原来的顺序表长度n增加至 F[k]-1
(为了保证数组有序,copyOf会在末尾生成0,用最后一位数来填充后面的0)
2、代码实现
package work.rexhao.search;
import java.util.Arrays;
/**
* 斐波那契查找Demo
* @Date 2022/7/4 22:36
*/
public class FibonacciSearch {
public static void main(String[] args) {
int[] num = new int[]{10, 12, 23, 32, 45, 64, 65, 69, 83};
System.out.println(Arrays.toString(num));
System.out.println(fibSearch(num, 69));
System.out.println(fibSearch(num, 99));
}
/**
* 生成斐波那契数列
*
* @param maxSize 元素最大个数
* @return 斐波那契数组
*/
public static int[] fib(int maxSize) {
int[] f = new int[maxSize];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < maxSize; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
/**
* 斐波那契查找
*
* @return 索引值
*/
public static int fibSearch(int[] num, int target) {
// 一、生成斐波那契数列
int[] f = fib(20);
// k:斐波那契分隔值的下表
int k = 0;
// 二、扩充数列
// 1.找到适合的fib[k]大小
while (num.length - 1 > f[k]) k++;
// 2.扩充并复制数组
int[] temp = Arrays.copyOf(num, f[k]);
// 3.为了保证数组有序,copyOf会在末尾生成0,用最后一位数来填充后面的0
for (int i = num.length; i < temp.length; i++) {
temp[i] = num[num.length - 1];
}
// 三、查找
int mid = 0;
int low = 0;
int high = num.length - 1;
while (low < high) {
mid = low + f[k - 1] - 1;
if (target > temp[mid]) {
// 比中间大 --> 找右边
low = mid + 1;
k -= 2;
} else if (target < temp[mid]) {
// 比中间小 --> 找左边
high = mid - 1;
k -= 1;
} else {
// 找到了
return mid;
}
}
// 没找到
return -1;
}
}哈希表
一、哈希表介绍
散列表(Hash table也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。
通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
二、代码实现
package work.rexhao.hashtab;
/**
* 哈希表
* @Date 2022/7/4 23:12
*/
public class HashTabDemo {
public static void main(String[] args) {
hashTab ht = new hashTab();
ht.add(new stu(10,"wmh","123123"));
ht.add(new stu(20,"wc","123123"));
System.out.println(ht.find(10));
System.out.println(ht.find(20));
System.out.println(ht.find(30));
}
}
/**
* student的实体类
*/
class stu {
private Integer no;
private String name;
private String phone;
public stu next;
stu(Integer no, String name, String phone) {
this.no = no;
this.name = name;
this.phone = phone;
}
@Override
public String toString() {
return "stu{" + "no=" + no + ", name='" + name + '\'' + ", phone='" + phone + '\'' + '}';
}
public Integer getNo() {
return no;
}
public String getName() {
return name;
}
public String getPhone() {
return phone;
}
public void setName(String name) {
this.name = name;
}
public void setNo(Integer no) {
this.no = no;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
/**
* hashTab
*/
class hashTab {
// 链表节点头部分
stu[] head = new stu[10];
/**
* 添加节点的方法(尾插法) 将最后的next域指向新的node
*/
public void add(stu s) {
if (s == null) {
return;
}
// 没有头指针,第一次添加需要直接放进head里面
if(head[s.getNo() % 10] == null){
head[s.getNo() % 10] = s;
return;
}
// 需要一个辅助变量
stu temp = head[s.getNo() % 10];
// 遍历单链表
while (temp.next != null) {
temp = temp.next;
}
// 退出循环时,temp指向最后
temp.next = s;
}
/**
* 查找
*/
public stu find(int no) {
// 判空
if (head[no % 10] == null) {
return null;
}
// 遍历
stu temp = head[no % 10];
while (temp != null) {
if (temp.getNo() == no) {
break;
}
temp = temp.next;
}
return temp;
}
}树结构基础
一、二叉树
1、为什么需要树
1)数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
2)链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链按到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
3)树存储方式的分析
可以提高数据存储,读取的效率
比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
2、树的常用术语
- 节点
- 根节点
- 父节点
- 子节点
- 叶子节点:没有子节点的节点
- 节点的权:节点值
- 路径:从root 节点找到该节点的路线
- 层
- 子树
- 树的高度(最大层数)
- 森林 :多颗子树构成森林
3、二叉树概述
- 二叉树:每个节点最多只能有两个子节点(左节点和右节点)
- 满二叉树:二叉树的所有叶子节点都在最后一层,并且结点总数=2^n-1(n 为层数)
- 完全二叉树:二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续
4、二叉树遍历概述
- 前序遍历:先输出父节点,再遍历左子树和右子树
- 中序遍历:先遍历左子树,再输出父节点,再遍历右子树
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点
小结:看输出父节点的顺序,就确定是前序,中序还是后序
5、二叉树代码实现
package work.rexhao.tree;
/**
* 二叉树的遍历和查找
* @Date 2022/7/5 10:23
*/
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
System.out.print("先序遍历: ");
BinaryTree.preOrder(bt.head);//先序遍历:12453
System.out.println();
System.out.print("中序遍历: ");
BinaryTree.infixOrder(bt.head);//中序遍历:42513
System.out.println();
System.out.print("后序遍历: ");
BinaryTree.postOrder(bt.head);//后续遍历:45231
System.out.println();
System.out.println("------------");
System.out.println("先序查找3: " + BinaryTree.preOrderSearch(bt.head, 3));
System.out.println("先序查找6: " + BinaryTree.preOrderSearch(bt.head, 6));
System.out.println("中序查找3: " + BinaryTree.infixOrderSearch(bt.head, 3));
System.out.println("中序查找6: " + BinaryTree.infixOrderSearch(bt.head, 6));
System.out.println("后序查找3: " + BinaryTree.postOrderSearch(bt.head, 3));
System.out.println("后序查找6: " + BinaryTree.postOrderSearch(bt.head, 6));
System.out.println("------------");
BinaryTree.delTree(bt.head, 2);
BinaryTree.infixOrder(bt.head);//中序遍历:13
System.out.println();
BinaryTree.delTree(bt.head, 6);
BinaryTree.infixOrder(bt.head);//中序遍历:13
System.out.println();
System.out.println("------------");
bt = new BinaryTree();
BinaryTree.delNode(bt.head, 2);
BinaryTree.infixOrder(bt.head);//中序遍历:453
System.out.println();
}
}
/**
* 二叉树
*/
class BinaryTree {
TreeNode head;
public BinaryTree() {
head = new TreeNode(1);
head.leftNode = new TreeNode(2);
head.rightNode = new TreeNode(3);
head.leftNode.leftNode = new TreeNode(4);
head.leftNode.rightNode = new TreeNode(5);
/*
初始化树结构示意图
1
/ \
2 3
/ \
4 5
先序遍历:12453
中序遍历:42513
后续遍历:45231
*/
}
/**
* 先序遍历
*/
public static void preOrder(TreeNode tn) {
if (tn != null) {
System.out.print(tn.getData());
preOrder(tn.leftNode);
preOrder(tn.rightNode);
}
}
/**
* 中序遍历
*/
public static void infixOrder(TreeNode tn) {
if (tn != null) {
infixOrder(tn.leftNode);
System.out.print(tn.getData());
infixOrder(tn.rightNode);
}
}
/**
* 后序遍历
*/
public static void postOrder(TreeNode tn) {
if (tn != null) {
postOrder(tn.leftNode);
postOrder(tn.rightNode);
System.out.print(tn.getData());
}
}
/**
* 先序查找
*/
public static boolean preOrderSearch(TreeNode tn, int target) {
if (tn == null) return false;
if (target == tn.getData()) {
return true;
} else if (preOrderSearch(tn.leftNode, target)) {
return preOrderSearch(tn.leftNode, target);
} else {
return preOrderSearch(tn.rightNode, target);
}
}
/**
* 中序查找
*/
public static boolean infixOrderSearch(TreeNode tn, int target) {
if (tn == null) return false;
if (infixOrderSearch(tn.leftNode, target)) {
return infixOrderSearch(tn.leftNode, target);
} else if (target == tn.getData()) {
return true;
} else {
return infixOrderSearch(tn.rightNode, target);
}
}
/**
* 后序查找
*/
public static boolean postOrderSearch(TreeNode tn, int target) {
if (tn == null) return false;
if (postOrderSearch(tn.leftNode, target)) {
return postOrderSearch(tn.leftNode, target);
} else if (postOrderSearch(tn.rightNode, target)) {
return postOrderSearch(tn.rightNode, target);
} else {
return target == tn.getData();
}
}
/**
* 删除子树
* 叶子结点:删除节点
* 非叶子节点:删除子树
* 注:不删除根节点
*/
public static void delTree(TreeNode tn, int target) {
if (tn == null) {
return;
}
if (tn.leftNode != null) {
if (tn.leftNode.getData() == target) {
tn.leftNode = null;
return;
} else {
delTree(tn.leftNode, target);
}
}
if (tn.rightNode != null) {
if (tn.rightNode.getData() == target) {
tn.rightNode = null;
return;
} else {
delTree(tn.rightNode, target);
}
}
}
/**
* 删除节点
* 叶子节点:删除
* 非叶子节点(一个子节点):替代原节点
* 非叶子节点(两个子节点):左节点代替原节点
*/
public static void delNode(TreeNode tn, int target) {
if (tn == null) return;
if (tn.leftNode != null) {
if (tn.leftNode.getData() == target) {
if (tn.leftNode.rightNode == null && tn.leftNode.leftNode == null) {
// 叶子节点
tn.leftNode = null;
} else if (tn.leftNode.rightNode != null && tn.leftNode.leftNode != null) {
// 两个节点
tn.leftNode = tn.leftNode.leftNode;
} else {
// 一个节点
if (tn.leftNode.leftNode != null) {
// 左节点
tn.leftNode = tn.leftNode.leftNode;
} else {
// 右节点
tn.leftNode = tn.leftNode.rightNode;
}
}
return;
} else {
delNode(tn.leftNode, target);
}
}
if (tn.rightNode != null) {
if (tn.rightNode.getData() == target) {
if (tn.rightNode.rightNode == null && tn.rightNode.leftNode == null) {
// 叶子节点
tn.rightNode = null;
} else if (tn.rightNode.rightNode != null && tn.rightNode.leftNode != null) {
// 两个节点
tn.rightNode = tn.rightNode.leftNode;
} else {
// 一个节点
if (tn.rightNode.leftNode != null) {
// 左节点
tn.rightNode = tn.rightNode.leftNode;
} else {
// 右节点
tn.rightNode = tn.rightNode.rightNode;
}
}
return;
} else {
delNode(tn.rightNode, target);
}
}
}
}
/**
* 二叉树的节点
*/
class TreeNode {
private int data;
TreeNode leftNode;
TreeNode rightNode;
TreeNode(int data) {
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
}二、顺序存储二叉树
1、顺序存储二叉树的说明
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组
2、顺序存储二叉树的特点
- 顺序二叉树通常只考虑完全二叉树
- 第n个元素的左子节点为
2*n+1 - 第n个元素的右子节点为
2*n+2 - 第n个元素的父节点为
(n-1)/2 - n:表示二叉树中的第几个元素(按0开始编号)
3、顺序存储二叉树代码实现
package work.rexhao.tree;
/**
* 顺序存储二叉树
* @Date 2022/7/5 14:56
*/
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
ArrBinaryTree abt = new ArrBinaryTree(7);
for (int i = 0; i < abt.data.length; i++) {
abt.data[i] = i + 1;
}
System.out.print("前序:");
ArrBinaryTree.preOrder(abt, 0);
System.out.println();
System.out.print("中序:");
ArrBinaryTree.infixOrder(abt, 0);
System.out.println();
System.out.print("后序:");
ArrBinaryTree.postOrder(abt, 0);
System.out.println();
}
}
class ArrBinaryTree {
int[] data;
ArrBinaryTree(int maxSize) {
data = new int[maxSize];
}
/**
* 先序遍历
*/
public static void preOrder(ArrBinaryTree abt, int index) {
// 数组越界
if (index >= abt.data.length) {
return;
}
System.out.print(abt.data[index]);
preOrder(abt, index * 2 + 1);
preOrder(abt, index * 2 + 2);
}
/**
* 中序遍历
*/
public static void infixOrder(ArrBinaryTree abt, int index) {
// 数组越界
if (index >= abt.data.length) {
return;
}
infixOrder(abt, index * 2 + 1);
System.out.print(abt.data[index]);
infixOrder(abt, index * 2 + 2);
}
/**
* 后序遍历
*/
public static void postOrder(ArrBinaryTree abt, int index) {
// 数组越界
if (index >= abt.data.length) {
return;
}
postOrder(abt, index * 2 + 1);
postOrder(abt, index * 2 + 2);
System.out.print(abt.data[index]);
}
}









