1. 线性表、队列和栈
1.1 顺序表(数组)
按顺序存储方式存储的线性表(存储位置在空间上是连续的),可随机存取O(1),但是不利于插入和删除操作O(n)。同时可用ArrayList
来实现顺序表,add()
、remove()
、size()
。
import java.util.Arrays;
public class SeqList {
private int length;
private Object[] array; //可以存引用数据类型,8大基本数据类型也可通过自动装箱转换成对应的包装类
private int initCapacity;
//1.有参构造初始化顺序表
public SeqList(int initCapacity) {
this.initCapacity = initCapacity;
this.array = new Object[initCapacity];
this.length = 0;
}
//2.计算顺序表的长度
public int getLength() {
return this.length;
}
//3.判断顺序表是否为空
public boolean isEmpty() {
return this.length == 0;
}
//4.判断顺序表是否为满
public boolean isFull() {
return this.length == this.initCapacity;
}
private void checkIndex(int index) { //检查索引是否合法
if (index < 0 || index > this.length) {
throw new RuntimeException("索引不合法!");
}
}
//5.根据索引获取元素
public Object getElem(int index) {
if (isEmpty()) {
throw new RuntimeException("顺序表为空!");
}
checkIndex(index);
return this.array[index];
}
//6.指定索引位置插入
public void insertElem(int index, Object object) {
if (isFull()) {
this.array = Arrays.copyOf(this.array, this.length * 2); //满了就2倍扩容
this.initCapacity = this.length * 2;
}
checkIndex(index);
for (int i = this.length - 1; i >= index; i--) { //index及以后的元素依次后移
this.array[i + 1] = this.array[i];
}
this.array[index] = object;
this.length++;
}
//7. 默认在末尾插入元素
public void insert(Object object) {
this.insertElem(this.length, object);
}
//8.删除并返回指定索引元素
public Object deleteElem(int index) {
if (isEmpty()) {
throw new RuntimeException("顺序表为空!");
}
checkIndex(index);
Object object = this.array[index];
for (int i = index; i < this.length; i++) { //后边元素依次往前覆盖
this.array[i] = this.array[i + 1];
}
this.length--;
return object;
}
//9.更改顺序表中某元素的值并返回之前的元素
public Object setElem(int index, Object object) {
this.checkIndex(index);
Object ob = this.array[index];
this.array[index] = object;
return ob;
}
//10.打印顺序表
public void print() {
for (int i = 0; i < this.length; i++) {
System.out.println(this.array[i]); //若想打印满足自己需求,可重写toString()方法
}
}
}
1.2 链表
按链式存储方式存储的线性表(存储位置在空间上可以不连续),方便插入和删除操作O(1),但是不支持随机访问O(n)。同时可用LinkedList
来实现双向链表,add()
、remove()
。
1.2.1 单链表
使用的单链表是带头结点的,更方便对只有一个结点时的链表的插入和删除操作
class LNode { //定义链表结点,可包含多种不同数据
public int data;
public Object object;
public LNode next;
public LNode(int data, Object object) {
this.data = data;
this.object = object;
}
@Override
public String toString() { //重写结点的toString方法,便于打印显示
return "LNode{" +
"data=" + data +
", object=" + object +
'}';
}
}
public class SingleLinkedList {
private final LNode head; //头结点,设置为常量
//1.无参构造初始化单向链表
public SingleLinkedList() {
this.head = new LNode(0, null); //头结点不存放具体数据
}
//2.在链表尾部插入数据(尾插法)
public void tailInsert(LNode lNode) {
LNode temp = this.head;
while (temp.next != null) { //找到next为空的结点,即尾结点
temp = temp.next;
}
temp.next = lNode;
}
//3.在链表的头部插入数据(头插法),可实现单链表的反转
public void headInsert(LNode lNode) {
lNode.next = this.head.next;
this.head.next = lNode;
}
//4.在指定位置插入数据
public void indexInsert(int index, LNode lNode) { //index:[1,length+1]
if (index < 1 || index > this.getLength() + 1) {
throw new RuntimeException("插入的位置不合法!");
}
LNode temp = getIndexLNodePrior(index); //找index前驱结点
lNode.next = temp.next;
temp.next = lNode;
}
//5.获取单向链表的长度,长度为0是则为空
public int getLength() {
LNode temp = this.head.next;
int count = 0;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
//6.根据索引修改结点信息,这里是用了先删除后添加完成修改,也可以直接更改对应位置的结点信息即可,next值不改
public void update(int index, LNode lNode) { //index:[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("修改的位置不合法!");
}
LNode temp = getIndexLNodePrior(index);
lNode.next = temp.next.next;
temp.next = lNode;
}
//7.根据索引返回该索引结点的前驱结点,方便插入、删除和修改
public LNode getIndexLNodePrior(int index) {
int count = 0; //默认头结点是第0个结点
LNode temp = this.head;
while (count < index - 1) { //找到插入第index个位置元素的位置
temp = temp.next;
count++;
}
return temp;
}
//8.根据索引删除结点并返回
public LNode delete(int index) { //index[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("删除的位置不合法!");
}
LNode temp = getIndexLNodePrior(index);
LNode lNode = temp.next;
temp.next = lNode.next;
return lNode;
}
//9.根据索引获取结点信息
public LNode getIndexLNode(int index) { //index:[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("获取结点的位置不合法!");
}
return getIndexLNodePrior(index).next;
}
//10.遍历打印单链表
public void print() {
LNode temp = this.head.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
}
1.2.2 循环单链表(约瑟夫环)
该循环单链表是不带头结点的,最后一个结点next指针指向第一个结点;插入时可定义一个辅助指针,当辅助指针的next指向第一个结点时代表到尾部了,借用单链表定义的LNode结点类,可解决约瑟夫环
问题。
public class CircleSingleLinkedList {
private LNode first = null; //采用了上边单链表的结点
//1.在循环单链表末尾添加元素
public void tailInsert(LNode lNode) {
if (this.first == null) { //当前循环链表为空
this.first = lNode;
this.first.next = this.first;
} else {
LNode temp = this.first;
while (temp.next != this.first) {
temp = temp.next;
}
lNode.next = temp.next;
temp.next = lNode;
}
}
//2.约瑟夫环
public void josephu(int m) { //每叫到第m个就删除该结点
if (m < 2) {
System.out.println("m必须为大于等于2的整数,否则无意义!");
return;
}
if (this.first == null) {
System.out.println("双向链表为空!");
return;
}
if (this.first.next == this.first) {
System.out.println("双向链表只有一个结点!");
System.out.println(this.first);
return;
}
int count;
LNode temp = this.first; //从第一个结点开始
while (temp.next != temp) { //当双向链表不只有一个结点时,一直循环
count = 1;
while (count < m - 1) { //找第m-1个结点,由于这是单向指针,只能先找前驱结点
temp = temp.next;
count++;
}
System.out.println(temp.next); //打印报数到m的结点并删除
temp.next = temp.next.next;
temp = temp.next; //将temp指向删除结点的后一个
}
System.out.println(temp); //此时双向链表中只包含一个结点
}
//3.打印单向循环链表
public void print() {
LNode temp = this.first;
while (temp.next != this.first) {
System.out.println(temp);
temp = temp.next;
}
System.out.println(temp);
}
}
1.2.3 双向链表
该双向链表带头结点,可向前和向后查找。循环双向链表(不带头结点,最后一个结点的后继是第一个结点,第一个结点的前驱是最后一个结点)
class DLNode {
public int data;
public Object object;
public DLNode prior;
public DLNode next;
public DLNode(int data, Object object) {
this.data = data;
this.object = object;
}
@Override
public String toString() {
return "DLNode{" +
"data=" + data +
", object=" + object +
'}';
}
}
public class DoubleLinkedList {
private final DLNode head;
public DoubleLinkedList() {
this.head = new DLNode(0, null);
}
//1.双向链表尾部添加结点(在哪里添加结点原理都类似,只要找到添加时对应的前驱结点或后继结点即可)
public void tailInsert(DLNode dlNode) {
DLNode temp = this.head;
while (temp.next != null) {
temp = temp.next;
}
dlNode.prior = temp;
temp.next = dlNode;
}
//2.删除指定位置的结点
public DLNode delete(int index) { //index:[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("删除时的索引位置不合法!");
}
DLNode temp = getIndexDLNode(index); //获取要删除的哪个结点
temp.prior.next = temp.next;
if (temp.next != null) { //注意是否是最后一个元素
temp.next.prior = temp.prior;
}
return temp;
}
//3.修改指定位置结点信息
public void update(int index, DLNode dlNode) { //index:[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("索引位置不合法!");
}
DLNode temp = getIndexDLNode(index);
temp.data = dlNode.data;
temp.object = dlNode.object;
}
//4.获取指定位置的结点
public DLNode getIndexDLNode(int index) { //index:[1,length]
if (index < 1 || index > this.getLength()) {
throw new RuntimeException("索引位置不合法!");
}
DLNode temp = this.head;
int count = 0;
while (count < index) {
temp = temp.next;
count++;
}
return temp;
}
//5.获取双向链表的长度
public int getLength() {
int len = 0;
DLNode temp = this.head;
while (temp.next != null) {
len++;
temp = temp.next;
}
return len;
}
//6.遍历双向链表(逆序遍历,可用栈,也可先找到尾结点然后用prior指针往前遍历直到头结点)
public void print() {
DLNode temp = this.head;
while (temp.next != null) {
temp = temp.next;
System.out.println(temp);
}
}
}
1.3 队列(FIFO)
队列是一种先进先出的线性表。可用LinkedList
实现双端队列
1.3.1 循环队列的顺序存储(数组)
此方法中少用一个存储空间;还可以定义一个flag变量,入队为1,出队为0,就可区分队空还是队满;同样可以定义一个len变量记录队列的长度,len为0则队空。
import java.util.Arrays;
public class SeqQueue {
private Object[] queue;
private int front;
private int rear;
private int initCapacity;
//1.有参构造初始化顺序存储队列
public SeqQueue(int initCapacity) {
this.queue = new Object[initCapacity];
this.front = 0; //默认队头和队尾都是从0开始,留出一个空白位置用于判断是否满了
this.rear = 0; //队头指向的是队头数据,队尾指向的就是队尾的下一个位置
this.initCapacity = initCapacity;
}
//2.计算队列长度
public int getLength() {
return (this.rear + this.initCapacity - this.front) % this.initCapacity;
}
//3.判断队列是否为空
public boolean isEmpty() {
return this.front == this.rear;
}
//4.判断队列是否满了
public boolean isFull() {
return (this.rear + 1) % this.initCapacity == this.front;
}
//5.将元素入队
public void enQueue(Object object) {
if (isFull()) {
this.queue = Arrays.copyOf(this.queue, this.initCapacity * 2); //队列满了自动扩容2倍
this.initCapacity *= 2;
}
this.queue[this.rear] = object;
this.rear = (this.rear + 1) % this.initCapacity;
}
//6.出队并返回元素
public Object deQueue() {
if (isEmpty()) {
throw new RuntimeException("队列为空,出队失败!");
}
Object object = this.queue[this.front];
this.front = (this.front + 1) % this.initCapacity;
return object;
}
//7.获取队头元素,但是不出队
public Object getFront() {
if (isEmpty()) {
throw new RuntimeException("队列为空,取队头元素失败!");
}
return this.queue[this.front];
}
//8.遍历队列
public void print() {
if (isEmpty()) {
throw new RuntimeException("队列为空,遍历失败!");
}
for (int i = this.front; i != this.rear; i = (i + 1) % this.initCapacity) {
System.out.println(this.queue[i].toString());
}
}
}
1.3.2 队列的链式存储
可用带头尾指针的单链表实现,仍借用单链表的结点类LNode
public class LinkedQueue {
private LNode front;
private LNode rear;
public LinkedQueue() {
this.front = this.rear = null;
}
//1.入队
public void enQueue(LNode lNode) {
if (this.front == null) { //队列为空
this.front = this.rear = lNode;
} else {
this.rear.next = lNode;
this.rear = lNode;
}
}
//2.出队
public LNode deQueue() {
if (this.front == null) {
throw new RuntimeException("队列为空,出队失败!");
}
LNode temp = null;
if (this.front == this.rear) { //队中只有一个数据
temp = this.front;
this.front = this.rear = null;
}
temp = this.front;
this.front = this.front.next;
return temp;
}
//3.打印队列
public void print() {
if (this.front == null) {
throw new RuntimeException("队列为空,遍历失败!");
}
LNode temp = this.front;
while (temp != this.rear) {
System.out.println(temp);
temp = temp.next;
}
System.out.println(temp);
}
}
1.4 栈(LIFO)
栈是一种后进先出的线性表。可用java.util.Stack<E>
来实现,empty()、peek()、pop()、push(E)。
1.4.1 栈的顺序存储
public class SeqStack {
private Object[] stack;
private int top;
private int initCapacity;
//1.有参构造初始化顺序栈
public SeqStack(int initCapacity) {
this.stack = new Object[initCapacity];
this.top = -1;
this.initCapacity = initCapacity;
}
//2.栈判空
public boolean isEmpty() {
return this.top == -1;
}
//3.栈判满
public boolean isFull() {
return this.top == this.initCapacity - 1;
}
//4.入栈
public void push(Object object) {
if (this.isFull()) {
this.stack = Arrays.copyOf(this.stack, this.initCapacity * 2);
this.initCapacity *= 2;
}
this.stack[++this.top] = object;
}
//5.出栈
public Object pop() {
if (this.isEmpty()) {
throw new RuntimeException("栈空,出栈失败!");
}
return this.stack[this.top--];
}
//6.求栈的长度
public int getLength() {
if (this.isEmpty()) {
return 0;
}
return this.top + 1;
}
//7.获取栈顶元素
public Object getTop() {
if (this.isEmpty()) {
throw new RuntimeException("栈为空,获取栈顶元素失败!");
}
return this.stack[this.top];
}
//8.打印栈的全部元素
public void print() {
if (this.isEmpty()) {
throw new RuntimeException("栈为空,打印失败!");
}
for (int i = this.top; i >= 0; i--) {
System.out.println(this.stack[i]);
}
}
}
1.4.2 栈的链式存储
采用了单链表定义的结点LNode类
public class LinkedStack {
private final LNode top = new LNode(0, null); //用头结点代表top,用头插法模拟栈
//1.无参构造初始化链栈
public LinkedStack() {
}
//2.入栈
public void push(LNode lNode) { //头插法入栈
lNode.next = this.top.next;
this.top.next = lNode;
}
//3.出栈
public LNode pop() {
if (this.top.next == null) {
throw new RuntimeException("链栈为空,出栈失败!");
}
LNode temp = this.top.next;
this.top.next = temp.next;
return temp;
}
//4.求链栈的长度
public int getLength() {
if (this.top.next == null) {
return 0;
}
LNode temp = this.top;
int len = 0;
while (temp.next != null) {
len++;
temp = temp.next;
}
return len;
}
//5.获取栈顶元素
public LNode getTop() {
if (this.top.next == null) {
throw new RuntimeException("链栈为空,获取栈顶元素失败!");
}
return this.top.next;
}
//6.遍历链栈
public void print() {
if (this.top.next == null) {
throw new RuntimeException("链栈为空,遍历失败!");
}
LNode temp = this.top;
while (temp.next != null) {
System.out.println(temp.next);
temp = temp.next;
}
}
}
1.5 稀疏数组(三元表)
用三元表实现稀疏矩阵的存储,可节省大量空间。三元表存储了稀疏矩阵中非零值的row、col和值,三元表第一个元素存放稀疏矩阵的行数、列数和总共的有效值个数。应用场景:棋盘、地图。
//1.将稀疏矩阵转换成三元表
public static int[][] sparseArrayToTernaryTables(int[][] sparseArray) {
int row = sparseArray.length; //求稀疏矩阵的行数和列数
int col = sparseArray[0].length;
int count = 0; //统计稀疏矩阵非零个数
for (int[] ints : sparseArray) {
for (int j = 0; j < col; j++) {
if (ints[j] != 0) {
count++;
}
}
}
int[][] ternaryTables = new int[count + 1][3];
ternaryTables[0][0] = row;
ternaryTables[0][1] = col;
ternaryTables[0][2] = count;
int index = 1; //记录三元表的下标
for (int i = 0; i < sparseArray.length; i++) {
for (int j = 0; j < sparseArray[i].length; j++) {
if (sparseArray[i][j] != 0) {
ternaryTables[index][0] = i;
ternaryTables[index][1] = j;
ternaryTables[index][2] = sparseArray[i][j];
index++;
}
}
}
return ternaryTables;
}
//2.将三元表转换成稀疏矩阵
public static int[][] ternaryTablesToSparseArray(int[][] ternaryTables) {
int row = ternaryTables[0][0];
int col = ternaryTables[0][1];
int count = ternaryTables[0][2]; //取出三元表中第一行存的稀疏数组的行数、列数和非0元素个数
int[][] sparseArray = new int[row][col];
for (int i = 1; i <= count; i++) {
sparseArray[ternaryTables[i][0]][ternaryTables[i][1]] = ternaryTables[i][2];
}
return sparseArray;
}
//3.打印二维数组
public static void print(int[][] array) {
for (int[] ints : array) {
for (int anInt : ints) {
System.out.printf("%d\t", anInt);
}
System.out.println();
}
}
1.6 栈的应用
栈的应用(中缀表达式的计算、中缀表达式转后缀表达式、后缀表达式的计算)
前缀表达式(波兰表达式):运算符位于操作数之前。
中缀表达式:我们使用的顺序。
后缀表达式(逆波兰表达式):运算符位于操作数之后。
1.6.1 中缀表达式的计算
定义一个操作数栈和一个运算符栈,遍历中缀表达式,遇到数字就入操作数栈,遇到界限符(左括号直接入运算符栈,右括号则将运算符栈出栈直到弹出左括号),遇到运算符就判断(若运算符栈为空,则直接入栈;若运算符栈不空,且栈顶元素运算符优先级高于或等于当前运算符,则将它们出栈直到栈空或遇到左括号,再把当前运算符入栈),每次运算符出栈就依次弹出两个操作数分别作为右操作数和左操作数,计算出结果后再将结果入操作数栈。遍历完中缀表达式后,将运算符依次出栈直到栈空,最后操作数栈中的唯一元素即为最终的结果。本方法采用了1.4.1的顺序栈来实现。
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CalculateMiddle {
public static void main(String[] args) {
SeqStack opStack = new SeqStack(20); //初始化操作数栈
SeqStack strStack = new SeqStack(20); //初始化运算符栈
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
Pattern p = Pattern.compile("(\\d+\\.?\\d*)|[+\\-*/]|[()]"); //获取正则表达对象(数字、运算符+-*/、括号)
Matcher m = p.matcher(str); //文本匹配器
while (m.find()) {
String s = m.group();
if (s.matches("\\d+\\.?\\d*")) { //操作数
opStack.push(Double.parseDouble(s));
} else if (s.matches("[+\\-*/]")) { //运算符
if (strStack.isEmpty()) { //栈空则直接入栈
strStack.push(s);
continue;
}
//若栈不空(必须放在第一个位置,否则无栈顶),且栈顶不是左括号,且栈顶优先级大于等于现在的运算符,则计算
while ((!strStack.isEmpty()) && (!strStack.getTop().toString().equals("(")) && isPriority(strStack.getTop().toString(),s)){
calculate(opStack, strStack);
}
strStack.push(s);
} else if (s.matches("[()]")) { //括号
if (s.equals("(")) {
strStack.push(s);
continue;
}
if (s.equals(")")) {
while (!strStack.getTop().toString().equals("(")) {
calculate(opStack, strStack);
}
strStack.pop(); //左括号则出栈
}
} else {
System.out.println("存在不可识别的字符,输入错误!");
}
}
while (!strStack.isEmpty()) {//运算符不空则出栈直到空为止
calculate(opStack, strStack);
}
System.out.println("结果为:" + opStack.getTop() + " " + opStack.getLength());
}
//比较两个运算符的优先级,若str1优先级大于等于str2则返回true
public static boolean isPriority(String str1, String str2) {
return getPriority(str1) - getPriority(str2) >= 0;
}
//获取优先级等级
public static int getPriority(String str) {
if (str.equals("*") || str.equals("/"))
return 1;
else if (str.equals("+") || str.equals("-"))
return 0;
else
return -1;
}
//从操作数中依次弹出两个操作数作为右操作数和左操作数,从运算符栈中弹出栈顶运算符并计算结果入操作数栈顶
public static void calculate(SeqStack opStack, SeqStack strStack) {
double op2 = (double) opStack.pop();
double op1 = (double) opStack.pop();
double result = 0;
switch (strStack.pop().toString()) {
case "+" -> result = op1 + op2;
case "-" -> result = op1 - op2;
case "*" -> result = op1 * op2;
case "/" -> result = op1 / op2;
}
opStack.push(result);
}
}
1.6.2 中缀表达式转后缀表达式
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。从左到右处理各个元素,直到末尾。可能遇到三种情况:
①遇到操作数
。直接加入后缀表达式。
②遇到界限符
。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
③遇到运算符
。栈空则直接入栈,非空则依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。
按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MiddleToSuffix {
public static void main(String[] args) {
SeqStack strStack = new SeqStack(20); //只定义一个操作符栈
Scanner scanner = new Scanner(System.in);
String str = scanner.nextLine();
Pattern p = Pattern.compile("(\\d+\\.?\\d*)|[+\\-*/]|[()]"); //获取正则表达对象(数字、运算符+-*/、括号)
Matcher m = p.matcher(str); //文本匹配器
while (m.find()) {
String s = m.group();
if (s.matches("\\d+\\.?\\d*")) { //操作数
System.out.printf("%s ", s);
} else if (s.matches("[+\\-*/]")) { //运算符
if (strStack.isEmpty()) { //栈空则直接入栈
strStack.push(s);
continue;
}
//若栈不空(必须放在第一个位置,否则无栈顶),且栈顶不是左括号,且栈顶优先级大于等于现在的运算符,则输出后缀表达式
while ((!strStack.isEmpty()) && (!strStack.getTop().toString().equals("(")) && isPriority(strStack.getTop().toString(), s)) {
System.out.printf("%s ", strStack.pop());
}
strStack.push(s);
} else if (s.matches("[()]")) { //括号
if (s.equals("(")) {
strStack.push(s);
continue;
}
if (s.equals(")")) {
while (!strStack.getTop().toString().equals("(")) {
System.out.printf("%s ", strStack.pop());
}
strStack.pop(); //左括号则出栈
}
} else {
System.out.println("存在不可识别的字符,输入错误!");
}
}
while (!strStack.isEmpty()) {//运算符不空则出栈直到空为止
System.out.printf("%s ", strStack.pop());
}
}
public static boolean isPriority(String str1, String str2) {
return getPriority(str1) - getPriority(str2) >= 0;
}
//获取优先级等级
public static int getPriority(String str) {
if (str.equals("*") || str.equals("/"))
return 1;
else if (str.equals("+") || str.equals("-"))
return 0;
else
return -1;
}
}
1.6.3 后缀表达式的计算
用栈实现后缀表达式的计算:(采用java.util.Stack<E>
来实现)
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到①
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
public class CalculateSuffix {
public static void main(String[] args) {
String suffixExpression = "3.2 5 2.8 * + 3 + 5 / 6 4 / 2 * + 3 +";
System.out.println("后缀表达式的计算结果为:" + calculateSuffix(getListString(suffixExpression)));
}
//将逆波兰表达式按空格进行分割存放到字符串列表中
public static List<String> getListString(String suffixExpression) {
String[] split = suffixExpression.split(" ");
List<String> arrayList = new ArrayList<String>(Arrays.asList(split));
return arrayList;
}
//计算后缀表达式的值
public static double calculateSuffix(List<String> ls) {
Stack<String> stack = new Stack<String>(); //只创建一个操作数栈即可
for (String l : ls) {
if (l.matches("\\d+\\.?\\d*")) { //操作数入栈
stack.push(l);
} else { //运算符,则弹出两个值计算结果再入栈
double num2 = Double.parseDouble(stack.pop());
double num1 = Double.parseDouble(stack.pop());
double result = 0;
switch (l) {
case "+" -> result = num1 + num2;
case "-" -> result = num1 - num2;
case "*" -> result = num1 * num2;
case "/" -> result = num1 / num2;
}
stack.push("" + result);
}
}
return Double.parseDouble(stack.peek());
}
}
2. 字符串
3. 树
3.1 二叉树
二叉树的每个结点最多只能有两个子结点。可以顺序存储和链式存储。
(1)满二叉树:二叉树每一层的结点数都达到最大值,结点总数为2^n-1。
(2)完全二叉树:满二叉树最后一层最后边的结点可以没有。
class LTNode {
public Object data;
public LTNode lChild;
public LTNode rChild;
public LTNode(Object data) {
this.data = data;
}
@Override
public String toString() {
return "LTNode{" + "data=" + data + '}';
}
}
public class BinaryTree {
private LTNode root;
}
3.1.1 二叉树的先序遍历
//先序遍历(递归)
public void preOrder(LTNode root) {
if (root != null) {
System.out.println(root);
preOrder(root.lChild);
preOrder(root.rChild);
}
}
//先序遍历(非递归,栈)
public void preOrderStack(LTNode root) {
if (root == null) {
return;
}
Stack<LTNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
LTNode top = stack.pop(); //先访问栈顶结点
System.out.println(top);
if (top.rChild != null) { //右孩子结点不空则先入栈,由于它是最后访问的
stack.push(top.rChild);
}
if (top.lChild != null) { //左孩子结点不空则后后入栈,先出
stack.push(top.lChild);
}
}
}
3.1.2 二叉树的中序遍历
//中序遍历(递归)
public void inOrder(LTNode root) {
if (root != null) {
inOrder(root.lChild);
System.out.println(root);
inOrder(root.rChild);
}
}
//中序遍历(非递归,栈)
public void inOrderStack(LTNode root) {
if (root == null) {
return;
}
Stack<LTNode> stack = new Stack<>();
LTNode temp = root;
while (temp != null || !stack.isEmpty()) {
if (temp != null) { //最左下结点
stack.push(temp);
temp = temp.lChild;
} else {
LTNode top = stack.pop(); //最左下结点弹出并遍历
System.out.println(top);
temp = top.rChild; //再查看最左下结点是否有右子结点
}
}
}
3.1.3 二叉树的后序遍历
//后序遍历(递归)
public void postOrder(LTNode root) {
if (root != null) {
postOrder(root.lChild);
postOrder(root.rChild);
System.out.println(root);
}
}
//后序遍历(非递归,栈)
public void postOrderStack(LTNode root) {
if (root == null) {
return;
}
Stack<LTNode> stack = new Stack<>();
LTNode temp = root, pre = null; //r用于标记上一个访问的是哪个
while (temp != null || !stack.isEmpty()) {
if (temp != null) { //最左下结点
stack.push(temp);
temp = temp.lChild;
} else {
LTNode top = stack.peek(); //获取栈顶结点,即最左下结点
if (top.rChild != null && top.rChild != pre) { //若它的右子结点非空且尚未访问,则还要先后序遍历该右子结点
temp = top.rChild;
} else { //到这里,证明最左下结点的子结点已经遍历完或为空,则访问该根结点
pre = stack.pop();
System.out.println(pre);
}
}
}
}
3.1.4 二叉树的层次遍历
//层次遍历
public void levelOrder(LTNode root) {
if (root == null) {
return;
}
Queue<LTNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) { //当队列不空,则出队,并将其左右子结点入队
LTNode temp = queue.remove();
System.out.println(temp);
if (temp.lChild != null) {
queue.add(temp.lChild);
}
if (temp.rChild != null) {
queue.add(temp.rChild);
}
}
}
3.1.5 由遍历序列构造二叉树
先序+中序;后序+中序;层次+中序。这三种可以构造唯一的二叉树。
3.2 线索二叉树
将二叉链表的空指针域存放某种遍历的前驱和后继的指针称将二叉树线索化。
class TBTNode {
public Object data;
public TBTNode lChild;
public TBTNode rChild;
public int lType; //0代表有左孩子结点,1代表指向的前驱
public int rType; //0代表有右孩子结点,1代表指向的后继
public TBTNode(Object data) {
this.data = data;
}
@Override
public String toString() {
return "TBTNode{" +
"data=" + data +
'}';
}
}
public class ThreadBinaryTree {
private TBTNode root;
private TBTNode pre = null; //为了线索化需要直到对应的前驱结点
}
根据中序遍历将二叉树线索化,并完成中序线索二叉树的遍历。先序线索化二叉树和后序线索化二叉树只需更改一下线索化当前结点的位置即可;
//中序线索化二叉树(递归)
public void threadTreeByInOrder(TBTNode node) {
if (node == null) {
return;
}
//线索化左子树(前提是左子树存在)
if (node.lType == 0) {
threadTreeByInOrder(node.lChild);
}
//线索化当前结点
if (node.lChild == null) { //如果当前结点的左子指针为空,则让它指向它的前驱结点
node.lChild = pre;
node.lType = 1;
}
if (pre != null && pre.rChild == null) { //若当前结点的前驱结点的右子指针为空,则让它指向它的后继结点
pre.rChild = node;
pre.rType = 1;
}
pre = node; //每次处理完一个结点,则该结点就变成了前驱结点
//线索化右子树(前提是右子树存在)
if (node.rType == 0) {
threadTreeByInOrder(node.rChild);
}
}
//遍历中序线索化的二叉树()
public void inOrderThreadTree(TBTNode root) {
TBTNode temp = root;
while (temp != null) {
while (temp.lType == 0) { //若lType一直为0,则证明左指针存的是左子树;跳出循环不为0,则证明存的为前驱结点
temp = temp.lChild;
}
System.out.println(temp); //打印当前结点
while (temp.rType == 1) { //若rType一直为1,则证明右指针存的是它的后继,跳出循环不为1,则证明存的是右子树
temp = temp.rChild;
System.out.println(temp); //打印它的后继结点
}
temp = temp.rChild; //替换当前的结点
}
}
//遍历先序线索二叉树
public void preOrderThreadTree(TBTNode root) {
TBTNode temp = root;
while (temp != null) {
System.out.println(temp); //打印当前结点
if (temp.lChild != null && temp.lType == 0) { //左子树
temp = temp.lChild;
} else {
temp = temp.rChild; //右子树或后继结点
}
}
}
3.3 哈夫曼树
哈夫曼树是带权路径长度最短的树,权值较大的结点离根比较近。
3.3.1 哈夫曼树的创建
哈夫曼树由于创建方式和相同权值的位序不同导致哈夫曼树并不唯一,但它的带权路径长度都是一样的。
//哈夫曼树的结点,实现Comparable接口,用于集合的排序
class HTNode implements Comparable<HTNode> {
public int data;
public HTNode lChild;
public HTNode rChild;
public HTNode(int data) {
this.data = data;
}
@Override
public String toString() {
return "HTNode{" + "data=" + data + '}';
}
@Override
public int compareTo(HTNode o) { //从小到大排序
return this.data - o.data;
}
}
//根据权值创建哈夫曼树
public HTNode createHuffmanTree(int[] array) {
List<HTNode> nodes = new ArrayList<>(); //为了方便排序创建集合
for (int value : array) {
nodes.add(new HTNode(value));
}
while (nodes.size() > 1) {
Collections.sort(nodes); //将集合从小到大进行排序
HTNode leftNode = nodes.remove(0);
HTNode rightNode = nodes.remove(0);
HTNode parentNode = new HTNode(leftNode.data + rightNode.data);
parentNode.lChild = leftNode;
parentNode.rChild = rightNode;
nodes.add(parentNode);
}
return nodes.get(0);
}
3.3.2 哈夫曼树编码
由于哈夫曼树是用叶子结点来存储数据,由于叶子结点的路径都是不同的,因此在用哈夫曼进行编码时都是唯一的,无二义性(即不存在某字符的编码是另一个字符编码的前缀的情况),但是由于哈夫曼树不同,所以哈夫曼编码可能不同,但是编码的整体效果一样。
import java.util.*;
//哈夫曼编码树的结点
class HCNode implements Comparable<HCNode> {
public Byte data; //数据
public int weight; //权重
public HCNode lChild;
public HCNode rChild;
public HCNode(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public String toString() {
return "HCNode{" +
"data=" + data +
", weight=" + weight +
'}';
}
@Override
public int compareTo(HCNode o) {
return this.weight - o.weight;
}
}
public class HuffmanCode {
public static void main(String[] args) {
HuffmanCode huffmanCode = new HuffmanCode();
String content = "i like java do you like java";
byte[] contentBytes = content.getBytes();
byte[] zipData = huffmanCode.huffmanZip(contentBytes);
byte[] result = huffmanCode.decodeData(zipData, huffmanCode.huffmanCodes);
System.out.println(new String(result));
}
//传入字符串,返回哈夫曼编码压缩后的字节数组
public byte[] huffmanZip(byte[] contentBytes) {
List<HCNode> nodes = this.getNodes(contentBytes); //获取结点(数据+权重)
HCNode root = this.createHuffmanCode(nodes); //创建哈夫曼编码树
this.getCodes(root, "", new StringBuilder()); //生成哈夫曼编码
return this.zipData(contentBytes, this.huffmanCodes); //将数据按照哈夫曼编码规则转换成8位一组的字节数组
}
//传入字节数组,解码为原来的字符串
public byte[] decodeData(byte[] zipData, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder();
for (byte zipDatum : zipData) {
//将字节转换成对应的二进制字符串 ,& 0xFF:只保留低8位,+0x100:对不够8位的数补0,substring(1):切割为8位数
stringBuilder.append(Integer.toBinaryString((zipDatum & 0xFF) + 0x100).substring(1));
}
//将哈夫曼编码转换成<编码,数据>的格式,即反转
Map<String, Byte> map = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
map.put(entry.getValue(), entry.getKey());
}
//遍历字符串并查找map中对应的数据
List<Byte> array = new ArrayList<>();
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;
while (true) {
if (i + count > stringBuilder.length()) { //当超过总长度也没有找到的时候,证明数据最后以为有冗余的补0了
i = stringBuilder.length();
break;
}
String key = stringBuilder.substring(i, i + count);
if (map.containsKey(key)) {
array.add(map.get(key));
break;
}
count++;
}
i += count;
}
byte[] result = new byte[array.size()];
for (int i = 0; i < array.size(); i++) {
result[i] = array.get(i);
}
return result;
}
//统计每个元素出现的次数,获取全部结点
public List<HCNode> getNodes(byte[] bytes) {
ArrayList<HCNode> nodes = new ArrayList<>();
Map<Byte, Integer> map = new HashMap<>();
for (byte b : bytes) {
Integer flag = map.getOrDefault(b, -1);
if (flag == -1) { //flag为-1证明map中并不存在b,则添加进去
map.put(b, 1);
} else {
map.put(b, flag + 1); //不是-1则把次数加1
}
}
//遍历map并将其存入数组集合中
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
nodes.add(new HCNode(entry.getKey(), entry.getValue()));
}
return nodes;
}
//根据结点信息创建哈夫曼树
public HCNode createHuffmanCode(List<HCNode> nodes) {
while (nodes.size() > 1) {
Collections.sort(nodes);
HCNode leftNode = nodes.remove(0);
HCNode rightNode = nodes.remove(0);
HCNode parentNode = new HCNode(null, leftNode.weight + rightNode.weight);
parentNode.lChild = leftNode;
parentNode.rChild = rightNode;
nodes.add(parentNode);
}
return nodes.get(0);
}
public Map<Byte, String> huffmanCodes = new HashMap<>(); //存放哈夫曼编码
根据哈夫曼树进行编码(左0右1),stringBuilder+code存放当前结点的哈夫曼编码
public void getCodes(HCNode node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
stringBuilder2.append(code);
if (node != null) {
if (node.data == null) { //非叶子结点
getCodes(node.lChild, "0", stringBuilder2); //左0右1
getCodes(node.rChild, "1", stringBuilder2);
} else { //叶子结点
huffmanCodes.put(node.data, stringBuilder2.toString()); //存入哈希表
}
}
}
//将数据按照哈夫曼编码规则转换成byte存储
public byte[] zipData(byte[] content, Map<Byte, String> huffmanCode) {
StringBuilder stringBuilder = new StringBuilder();
for (byte b : content) {
stringBuilder.append(huffmanCode.get(b)); //将数据转换成哈夫曼编码后的二进制字符串
}
if (stringBuilder.length() % 8 != 0) { //不足8位后边补0
stringBuilder.append("0".repeat(8 - stringBuilder.length() % 8));
}
int len = stringBuilder.length() / 8;
byte[] zipData = new byte[len]; //存的是补码,负数原码符号位不变,其余取反得到反码,反码+1得到补码
for (int i = 0; i < len; i++) {
zipData[i] = (byte) Integer.parseInt(stringBuilder.substring(i * 8, i * 8 + 8), 2);
}
return zipData;
}
}
3.3.3 文件压缩与解压
调用上部分的HuffmanCode类来实现将读取的文件进行压缩与解压。适用于二进制文件、文本文件,若文件中重复的数据不多,则压缩效果也不会很明显。
import java.io.*;
import java.util.Map;
public class FileHuffman {
public static void main(String[] args) {
String srcFile = "D:\\src.pdf";
String dstFile = "D:\\dst.zip";
String dstFile2 = "D:\\src_fu.pdf";
zipFile(srcFile, dstFile);
decodeFile(dstFile, dstFile2);
}
//压缩文件
public static void zipFile(String srcFile, String dstFile) {
try {
FileInputStream inputStream = new FileInputStream(srcFile);
byte[] contents = new byte[inputStream.available()]; //创建和文件一样大小的byte数组
inputStream.read(contents);
inputStream.close();
HuffmanCode huffmanCode = new HuffmanCode();
byte[] huffmanBytes = huffmanCode.huffmanZip(contents);//对读取的字节数组进行哈夫曼编码
FileOutputStream outputStream = new FileOutputStream(dstFile);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); //关联文件输出流
objectOutputStream.writeObject(huffmanBytes); //把哈夫曼编码后的字节数组写入压缩文件
//以对象流的方式写入哈夫曼编码,为了更好的恢复源文件使用
objectOutputStream.writeObject(huffmanCode.huffmanCodes);
outputStream.close();
objectOutputStream.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
//解压文件
public static void decodeFile(String zipFile, String dstFile) {
try {
FileInputStream inputStream = new FileInputStream(zipFile);
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
byte[] huffmanBytes = (byte[]) objectInputStream.readObject(); //根据对象分别读取哈夫曼编码数组和哈夫曼编码
Map<Byte, String> huffmanCodes = (Map<Byte, String>) objectInputStream.readObject();
HuffmanCode huffmanCode = new HuffmanCode();
byte[] bytes = huffmanCode.decodeData(huffmanBytes, huffmanCodes);
FileOutputStream fileOutputStream = new FileOutputStream(dstFile);
fileOutputStream.write(bytes);
inputStream.close();
objectInputStream.close();
fileOutputStream.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
3.3 二叉排序树(BST)
二叉排序树:任何一个非叶子结点,左子树上所有结点的值都比该结点小,右子树上所有结点的值都比该结点大。
import java.util.Stack;
class BSTNode {
public int value;
public BSTNode lChild;
public BSTNode rChild;
public BSTNode(int value) {
this.value = value;
}
//添加结点
public void addNode(BSTNode node) {
if (node.value < this.value) { //添加在左子树上
if (this.lChild == null) {
this.lChild = node;
} else {
this.lChild.addNode(node);
}
} else {
if (this.rChild == null) {
this.rChild = node;
} else {
this.rChild.addNode(node);
}
}
}
@Override
public String toString() {
return "BSTNode{" +
"data=" + value +
'}';
}
}
public class BinarySortTree {
private BSTNode root;
public static void main(String[] args) {
BinarySortTree tree = new BinarySortTree();
int[] array = {7, 3, 10, 12, 5, 1, 9};
for (int item : array) {
tree.addNode(new BSTNode(item));
}
tree.deleteNode(7);
tree.inOrderBST();
}
//添加结点
public void addNode(BSTNode node) {
if (this.root == null) { //如果此树为空,则直接该结点就是根结点
this.root = node;
} else {
this.root.addNode(node);
}
}
//删除结点(定义两个指针,一个指向父亲,一个指向孩子,最终找到哪个孩子结点删除)
//1.删除的是叶子结点,直接删除
//2.删除的结点只有一个子结点,令它的父结点指向它的孩子结点
//3.删除的结点有左右子树,则从右子树中找最小的结点(最左下结点)删除
public void deleteNode(int value) {
if (root == null) {
System.out.println("该二叉排序树为空,删除结点失败!");
return;
}
BSTNode parent = null;
BSTNode child = this.root;
if (child.lChild == null && child.rChild == null) { //只有一个结点的时候
this.root = null;
return;
}
while (child != null) {
if (child.value == value) {
if (child.lChild == null && child.rChild == null) { //叶子结点
if (parent.lChild == child) {
parent.lChild = null;
} else {
parent.rChild = null;
}
} else if (child.lChild != null && child.rChild != null) { //删除的结点存在左右子结点
BSTNode temp = child.rChild;
while (temp.lChild != null) { //查找右子树的最左下结点
temp = temp.lChild;
}
this.deleteNode(temp.value); //转成删除最左下结点
child.value = temp.value; //将最左下结点的值赋值给当前要删除的结点
} else if (child.lChild != null) { //删除的结点的左子树不为空
if (parent != null) { //当只有两个结点的时候得预防一下是否为空
if (parent.lChild == child) {
parent.lChild = child.lChild;
} else {
parent.rChild = child.lChild;
}
} else {
this.root = child.lChild;
}
} else { //删除的结点的右子树不为空
if (parent != null) {
if (parent.lChild == child) {
parent.lChild = child.rChild;
} else {
parent.rChild = child.rChild;
}
} else {
this.root = child.rChild;
}
}
return;
} else if (value < child.value) {
parent = child;
child = child.lChild;
} else {
parent = child;
child = child.rChild;
}
}
System.out.println("该结点不存在,删除结点失败!"); //若循环完毕仍未找到该数据,则证明不存在
}
//BST树的中序遍历就相当于升序排序
public void inOrderBST() {
if (this.root == null) {
return;
}
Stack<BSTNode> stack = new Stack<>();
BSTNode temp = this.root;
while (temp != null || !stack.isEmpty()) {
if (temp != null) {
stack.push(temp);
temp = temp.lChild;
} else {
BSTNode top = stack.pop();
System.out.println(top);
temp = top.rChild;
}
}
}
}
3.4 平衡二叉树(AVL)
平衡二叉树是一颗空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两颗子树都是平衡二叉树。它可保证查询效率较高。
import java.util.Stack;
class AVLNode {
public int value;
public AVLNode lChild;
public AVLNode rChild;
public AVLNode(int value) {
this.value = value;
}
//平衡二叉树添加结点,返回值为根结点
public AVLNode addNode(AVLNode node, AVLNode root) {
//先按照二叉排序树的顺序对结点进行添加
if (node.value < this.value) {
if (this.lChild == null) {
this.lChild = node;
} else {
this.lChild.addNode(node, root);
}
} else {
if (this.rChild == null) {
this.rChild = node;
} else {
this.rChild.addNode(node, root);
}
}
//对最小的非平衡二叉子树进行旋转处理,若最小非平衡二叉树的根是整个平衡二叉树的根,则需要重置根结点
if (this.getRightHeight() - this.getLeftHeight() > 1) {
if (this.rChild.getRightHeight() > this.rChild.getLeftHeight()) { //符合RR型
if (this == root) { //若最小平衡二叉树的根是整个平衡二叉树的根,则重置根结点
root = this.rChild;
}
this.leftRotate();
} else { //符合RL型
if (this == root) {
root = this.rChild.lChild;
}
this.rightThenLeftRotate();
}
} else if (this.getLeftHeight() - this.getRightHeight() > 1) {
if (this.lChild.getLeftHeight() > this.lChild.getRightHeight()) { //符合LL型
if (this == root) {
root = this.lChild;
}
this.rightRotate();
} else { //符合LR型
if (this == root) {
root = this.lChild.rChild;
}
this.leftThenRightRotate();
}
}
return root;
}
//求平衡二叉树的高度
public int getHeight() {
return Math.max(this.lChild == null ? 0 : this.lChild.getHeight(), this.rChild == null ? 0 : this.rChild.getHeight()) + 1;
}
//求该结点的左子树的高度
public int getLeftHeight() {
if (this.lChild == null) {
return 0;
}
return this.lChild.getHeight();
}
//求该结点的右子树的高度
public int getRightHeight() {
if (this.rChild == null) {
return 0;
}
return this.rChild.getHeight();
}
//以当前结点进行左旋操作,单向左旋(RR型): 插入位置为右子树的右子树,右子树为轴心,进行单次向左旋转
public void leftRotate() {
AVLNode temp = this.rChild; //定义一个指针指向右子树
this.rChild = temp.lChild; //断链,将当前结点的右子树改为右子树的左子树结点
temp.lChild = this;
}
//以当前结点进行右旋操作,单向右旋(LL型): 插入位置为左子树的左子树,以左子树为轴心,进行单次向右旋转
public void rightRotate() {
AVLNode temp = this.lChild;
this.lChild = temp.rChild;
temp.rChild = this;
}
//以当前结点先左后右双旋,(LR型):插入位置为左子树的右子树,要进行两次旋转,先向左旋转,再向右旋转
public void leftThenRightRotate() {
AVLNode temp = this.lChild.rChild;
this.lChild.rChild = temp.lChild;
temp.lChild = this.lChild;
this.lChild = temp; //让该结点的左子结点先左旋
this.rightRotate(); //该结点再进行右旋
}
//以当前结点先右后左双旋,(RL型):插入位置为右子树的左子树,进行两次调整,先右旋转再左旋转
public void rightThenLeftRotate() {
AVLNode temp = this.rChild.lChild;
this.rChild.lChild = temp.rChild;
temp.rChild = this.rChild;
this.rChild = temp; //让该结点的右子结点先右旋
this.leftRotate(); //该结点再进行左旋
}
@Override
public String toString() {
return "AVLNode{" +
"value=" + value +
'}';
}
}
public class AVLTree {
public AVLNode root;
public static void main(String[] args) {
AVLTree tree = new AVLTree();
int[] array = {10, 11, 7, 6, 8, 9};
for (int item : array) {
tree.addNode(new AVLNode(item));
}
tree.inOrderAVL();
}
//平衡二叉树添加结点
public void addNode(AVLNode node) {
if (this.root == null) {
this.root = node;
} else {
this.root = this.root.addNode(node, this.root);
}
}
//BST树的中序遍历就相当于升序排序
public void inOrderAVL() {
if (this.root == null) {
return;
}
Stack<AVLNode> stack = new Stack<>();
AVLNode temp = this.root;
while (temp != null || !stack.isEmpty()) {
if (temp != null) {
stack.push(temp);
temp = temp.lChild;
} else {
AVLNode top = stack.pop();
System.out.println(top);
temp = top.rChild;
}
}
}
}
3.5 红黑树
红黑树是一个二叉排序树,但是不是高度平衡的,它的平衡是通过红黑规则实现的:
(1)每一个节点或是红色的,或者是黑色的
(2)根节点必须是黑色
(3)如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
(4)如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
(5)对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点;
添加结点:默认添加的是红色结点。 红黑树的增删改查效果都特别好,由于没有大量旋转,只是有很多颜色的改变。
3.6 B树(B-树)
B-树是一种平衡的多路查找树,注意: B树就是B-树,"-"是个连字符号,不是减号 。我们假设我们的数据量达到了亿级别,主存当中根本存储不下,我们只能以块的形式从磁盘读取数据,与主存的访问时间相比,磁盘的 I/O 操作相当耗时,而提出 B-树的主要目的就是减少磁盘的 I/O 操作(通过增加结点中关键字索引的数量,来减少树的高度,进而减少访问磁盘的次数)。
B树,又称多路平衡查找树,B树在非叶子结点存储了数据
,B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:
(1)树中每个结点至多有m棵子树,即至多含有m-1个关键字。
(2)若根结点不是终端结点,则至少有两棵子树。
(3)除根结点外的所有非叶结点至少有⌈m/2⌉棵子树,即至少含有⌈m/2⌉-1个关键字。
(5)所有的叶结点都出现在同一层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。
(6)非叶子结点结构如下:
3.7 B+树
B+树非叶子结点不存储数据,只存储索引
,叶子结点存储全部数据,并且所有叶子结点是用链表链接起来的(该链表是有序的)。更适用于文件索引系统。
4. 图
4.1 图的存储-邻接矩阵
邻接矩阵适合存储边比较密集的稠密图。
import java.util.ArrayList;
import java.util.Arrays;
//无向图-邻接矩阵
public class GraphVertex {
private ArrayList<String> vertexList; //存储顶点信息
private int[][] edges; //邻接矩阵
private int numEdges; //边的总个数
public GraphVertex(int n) {
vertexList = new ArrayList<>();
edges = new int[n][n];
numEdges = 0;
}
//插入结点
public void insertVertex(String vertex) {
this.vertexList.add(vertex);
}
//插入边,这里是无向图,有向图则删除edges[v2][v1] = weight;语句即可
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numEdges++;
}
//求结点数
public int getVertexNumber() {
return this.vertexList.size();
}
//求边的个数
public int getNumEdges() {
return this.numEdges;
}
//根据索引求结点的值
public String getValueByIndex(int i) {
return this.vertexList.get(i);
}
//求权值
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
//打印图
public void showGraph() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
public static void main(String[] args) {
GraphVertex graph = new GraphVertex(5);
String[] vertexValue = {"A", "B", "C", "D", "E"};
for (String s : vertexValue) {
graph.insertVertex(s);
}
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
graph.showGraph();
}
}
4.2 图的存储-邻接表
邻接表适合存储边比较少的稀疏图。
import java.util.ArrayList;
//创建边结点
class GNode {
public int adjvex; //指向哪个结点的索引,同时可以再加一个权重变量
public GNode next; //下一个指向的结点
public GNode(int adjvex) {
this.adjvex = adjvex;
}
}
//创建顶点列表类
class AdjList {
private ArrayList<String> vertexInfo; //顶点信息
private GNode[] first; //第一条边
public AdjList(int n) {
this.vertexInfo = new ArrayList<>();
first = new GNode[n];
}
//插入顶点
public void insertVertex(String vertex) {
vertexInfo.add(vertex);
}
//插入边(根据索引)
public void insertEdge(int v1, int v2) {
if (this.first[v1] == null) { //判断是否为空
this.first[v1] = new GNode(v2);
} else { //头插法插入链表
GNode temp = new GNode(v2);
temp.next = this.first[v1].next;
this.first[v1].next = temp;
}
if (this.first[v2] == null) { //判断是否为空
this.first[v2] = new GNode(v1);
} else { //头插法插入链表
GNode temp = new GNode(v1);
temp.next = this.first[v2].next;
this.first[v2].next = temp;
}
}
//插入边(根据顶点信息)
public void insertEdge(String v1, String v2) {
int indexV1 = vertexInfo.indexOf(v1);
int indexV2 = vertexInfo.indexOf(v2);
insertEdge(indexV1, indexV2);
}
//打印图的信息
public void showGraph() {
for (int i = 0; i < first.length; i++) {
System.out.print("与结点" + vertexInfo.get(i) + "的有边的结点有:");
while (first[i] != null) {
if (first[i].next != null) {
System.out.print(vertexInfo.get(first[i].adjvex) + "=>");
} else {
System.out.print(vertexInfo.get(first[i].adjvex));
}
first[i] = first[i].next;
}
System.out.println();
}
}
}
public class GraphLinked {
public static void main(String[] args) {
AdjList graph = new AdjList(5);
String[] vertexValue = {"A", "B", "C", "D", "E"};
for (String s : vertexValue) {
graph.insertVertex(s);
}
graph.insertEdge(0, 1);
graph.insertEdge(0, 2);
graph.insertEdge(1, 2);
graph.insertEdge(1, 3);
graph.insertEdge(1, 4);
graph.showGraph();
}
}
4.3 图的深度优先遍历(DFS)
使用图的邻接矩阵进行存储时的深度优先遍历。1. 图可能是非连通的;2. 遍历完一个结点,就去找该结点尚未被访问的邻接结点,重复该操作。
//图可能是非连通的
public void deepFirstSearch() {
boolean[] isVisit = new boolean[vertexList.size()]; //用于记录是否被访问
for (int i = 0; i < vertexList.size(); i++) { //从第一个结点开始遍历
if (!isVisit[i]) {
deepFirstSearch(vertexList.get(i), isVisit);
}
}
}
//深度优先遍历图(邻接矩阵存储的无向图,递归实现)
public void deepFirstSearch(String vertex, boolean[] isVisit) {
int index = vertexList.indexOf(vertex);
System.out.print(vertex + " ");
isVisit[index] = true;
for (int i = 0; i < edges.length; i++) {
if (edges[index][i] > 0 && !isVisit[i]) { //没有被访问的邻接点
deepFirstSearch(vertexList.get(i), isVisit);
}
}
}
4.4 图的广度优先遍历(BFS)
通过使用队列,依次访问结点尚未访问的邻接结点,通过不断入队和出队来实现。
//图可能是非连通的
public void breadFirstSearch() {
boolean[] isVisit = new boolean[vertexList.size()]; //用于记录是否被访问
for (int i = 0; i < vertexList.size(); i++) { //从第一个结点开始遍历
if (!isVisit[i]) {
breadFirstSearch(vertexList.get(i), isVisit);
}
}
}
//广度优先遍历图(邻接矩阵存储的无向图,队列实现)
public void breadFirstSearch(String vertex, boolean[] isVisit) {
int index = vertexList.indexOf(vertex);
Queue<Integer> queue = new LinkedList<>();
System.out.print(vertexList.get(index) + " "); //先访问第一个结点
isVisit[index] = true;
queue.add(index);
while (!queue.isEmpty()) {
int temp = queue.remove();
for (int i = 0; i < vertexList.size(); i++) {
if (edges[temp][i] > 0 && !isVisit[temp]) {
System.out.print(vertexList.get(index) + " ");
isVisit[index] = true;
queue.add(i);
}
}
}
}
4.5 图的最小生成树
- 普利姆(Prim)算法
选择点,O(n2)(n为顶点数),适合稠密图。
public static class Edge implements Comparable<Edge> { // 创建类-代表边 int source; // 边的起点 int destination; // 边的终点 int weight; // 边的权重 public Edge(int source, int destination, int weight) { this.source = source; this.destination = destination; this.weight = weight; } @Override public int compareTo(Edge other) { // 重写比较方法,按权重进行比较 return this.weight - other.weight; } } public static List<Edge> primMST(int n, int[][] edges) { // 构建邻接表形式的图 List<List<Edge>> graph = new ArrayList<>(); for (int i = 0; i < n; i++) { graph.add(new ArrayList<>()); } for (int[] edge : edges) { int from = edge[0]; int to = edge[1]; int weight = edge[2]; graph.get(from).add(new Edge(from, to, weight)); // 无向图 graph.get(to).add(new Edge(to, from, weight)); } List<Edge> result = new ArrayList<>(); boolean[] visited = new boolean[n]; PriorityQueue<Edge> minHeap = new PriorityQueue<>(); // 优先级队列-小顶堆 // 从顶点0开始 visited[0] = true; minHeap.addAll(graph.get(0)); // 将顶点0可达的边全部加入到小顶堆优先级队列 while (!minHeap.isEmpty()) { Edge edge = minHeap.poll(); // 取权重最小的边并删除 int destination = edge.destination; if (visited[destination]) { // 若该顶点已经被访问,则继续找下一个最小的边 continue; } visited[destination] = true; // 没访问过则标记该顶点,并把该边加入结果集 result.add(edge); for (Edge nextEdge : graph.get(destination)) { if (!visited[nextEdge.destination]) { // 把以该顶点为起点的且终点尚未访问的边加入小顶堆中 minHeap.add(nextEdge); } } } return result; } public static void main(String[] args) { int n = 5; int[][] edges = { {0, 1, 2}, {0, 3, 6}, {1, 2, 3}, {1, 3, 8}, {1, 4, 5}, {2, 4, 7}, {3, 4, 9} }; List<Edge> mstEdges = primMST(n, edges); System.out.println("Prim's Minimum Spanning Tree Edges:"); for (Edge edge : mstEdges) { System.out.println(edge.source + " - " + edge.destination + " : " + edge.weight); } }
- 克鲁斯卡尔(Kruskal)算法
选择边,O(eloge)(e为边数),适合稀疏图。
public static class Edge implements Comparable<Edge> { // 创建类-代表边 int source; // 边的起点 int destination; // 边的终点 int weight; // 边的权重 public Edge(int source, int destination, int weight) { this.source = source; this.destination = destination; this.weight = weight; } @Override public int compareTo(Edge other) { // 重写比较方法,按权重进行比较 return this.weight - other.weight; } } // 并查集保证在添加边的过程中不形成环路,find操作,查找某个节点所在的集合的根节点 public static int findParent(int[] parent, int vertex) { if (parent[vertex] == vertex) { // 根节点 return vertex; } return findParent(parent, parent[vertex]); } public static List<Edge> kruskalMST(int n, int[][] edges) { List<Edge> result = new ArrayList<>(); List<Edge> allEdges = new ArrayList<>(); // 所有的边 for (int[] edge : edges) { int from = edge[0]; int to = edge[1]; int weight = edge[2]; allEdges.add(new Edge(from, to, weight)); } Collections.sort(allEdges); // 将所有的边从小到大排序 int[] parent = new int[n]; for (int i = 0; i < n; i++) { // 初始时每个顶点都是一个独立的集合(连通分量) parent[i] = i; } int edgeCount = 0; // 当前边的个数 int index = 0; while (edgeCount < n - 1) { Edge currentEdge = allEdges.get(index++); // 依次取最小的边 int sourceParent = findParent(parent, currentEdge.source); int destinationParent = findParent(parent, currentEdge.destination); if (sourceParent != destinationParent) { // 不相等证明两者不在一个连通分量中 result.add(currentEdge); edgeCount++; parent[sourceParent] = destinationParent; // union操作,合并,destinationParent和sourceParent可互换位置 } } return result; } public static void main(String[] args) { int n = 5; int[][] edges = { {0, 1, 2}, {0, 3, 6}, {1, 2, 3}, {1, 3, 8}, {1, 4, 5}, {2, 4, 7}, {3, 4, 9} }; List<Edge> mstEdges = kruskalMST(n, edges); System.out.println("Kruskal's Minimum Spanning Tree Edges:"); for (Edge edge : mstEdges) { System.out.println(edge.source + " - " + edge.destination + " : " + edge.weight); } }
4.6 图的最短路径
- 迪杰斯特拉(Dijkstra)算法-单源最短路径
时间复杂度O(n*n),但只适用于权重为正数的情况。
public class DijkstraAlgorithm { public int findCheapestPriceDijkstra(int n, int[][] flights, int src, int dst) { // 邻接矩阵存储有向图 int[][] matrix = new int[n][n]; for (int i = 0; i < n; i++) { Arrays.fill(matrix[i], Integer.MAX_VALUE); } for (int[] flight : flights) { matrix[flight[0]][flight[1]] = flight[2]; } // 创建distances数组用于存储从src到各个节点的最短距离 int[] distances = Arrays.copyOf(matrix[src], n); // 创建visited数组用于判断是否已经确定 boolean[] visited = new boolean[n]; visited[src] = true; for (int i = 0; i < n; i++) { // 取没有确定的且距离最小的顶点 int minVertex = src; for (int j = 0; j < n; j++) { if (!visited[j] && distances[j] < distances[minVertex]) { minVertex = j; } } visited[minVertex] = true; if (minVertex == dst) { return distances[minVertex]; } // 遍历从minVertex可达的边,更新distances for (int j = 0; j < n; j++) { if (!visited[j] && matrix[minVertex][j] != Integer.MAX_VALUE && distances[minVertex] + matrix[minVertex][j] < distances[j]) { distances[j] = distances[minVertex] + matrix[minVertex][j]; } } } return -1; } public static void main(String[] args) { int n = 5; int[][] flights = {{0, 1, 5}, {1, 2, 5}, {0, 3, 2}, {3, 1, 2}, {1, 4, 1}, {4, 2, 1}}; int src = 0, dst = 2; DijkstraAlgorithm solution = new DijkstraAlgorithm(); int result = solution.findCheapestPriceDijkstra(n, flights, src, dst); System.out.println("Cheapest price using Dijkstra: " + result); } }
- 贝尔曼福德(Bellman-Ford)算法-单源最短路径
时间复杂度O(n*e)
,其中n代表顶点数,e代表边数,权重可以为负数,若经过n-1条边仍有数据在发生改变,则证明存在环(且一定为负环,因为这样距离才会减少,即更短),数组p可以存放当前节点的上一个节点,进而找到完整的路径。
public class BellmanFordAlgorithm { public int findCheapestPriceBellmanFord(int n, int[][] flights, int src, int dst, int k) { int[] distances = new int[n]; Arrays.fill(distances, Integer.MAX_VALUE); distances[src] = 0; // 初始化distances,只有src为0,其它都是无穷大 for (int i = 0; i <= k; i++) { // 循环k次代表从src到dst最多经过k条边的最短路径 int[] tempDistances = Arrays.copyOf(distances, n); // 创建临时数组存储上一轮的距离 for (int[] flight : flights) { int fromCity = flight[0]; int toCity = flight[1]; int price = flight[2]; // 若from + price < to,则替换to(即有更低价格到达to的路径) if (distances[fromCity] != Integer.MAX_VALUE && distances[fromCity] + price < tempDistances[toCity]) { tempDistances[toCity] = distances[fromCity] + price; } } distances = tempDistances; } return distances[dst] == Integer.MAX_VALUE ? -1 : distances[dst]; } public static void main(String[] args) { int n = 5; int[][] flights = {{0, 1, 5}, {1, 2, 5}, {0, 3, 2}, {3, 1, 2}, {1, 4, 1}, {4, 2, 1}}; int src = 0, dst = 2, k = 2; BellmanFordAlgorithm solution = new BellmanFordAlgorithm(); int result = solution.findCheapestPriceBellmanFord(n, flights, src, dst, k); System.out.println("Cheapest price using Bellman-Ford: " + result); } }
- 弗罗伊德(Floyd)算法-所有顶点间的最短路径
public class FloydWarshallAlgorithm { public void floydWarshall(int n, int[][] flights) { // 初始化邻接矩阵(对角线为0,其它为无穷大) int[][] distances = new int[n][n]; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { distances[i][j] = (i == j) ? 0 : Integer.MAX_VALUE; } } // 更新邻接矩阵 for (int[] flight : flights) { int from = flight[0]; int to = flight[1]; int price = flight[2]; distances[from][to] = price; } // 依次尝试经过每一个节点,即ik + kj < ij则替换 for (int k = 0; k < n; k++) { for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { if (distances[i][k] != Integer.MAX_VALUE && distances[k][j] != Integer.MAX_VALUE && distances[i][k] + distances[k][j] < distances[i][j]) { distances[i][j] = distances[i][k] + distances[k][j]; } } } } // 打印 for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { System.out.print(distances[i][j] + " "); } System.out.println(); } } public static void main(String[] args) { int n = 5; int[][] flights = { {0, 1, 5}, {1, 2, 5}, {0, 3, 2}, {3, 1, 2}, {1, 4, 1}, {4, 2, 1} }; FloydWarshallAlgorithm solution = new FloydWarshallAlgorithm(); solution.floydWarshall(n, flights); } }
4.7 图的拓扑排序
有向无环图:无环的有向图,简称DAG图
(Directed Acycline Graph)
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网
(Activity On Vertex network)。
在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV 网中有弧<i, j>存在,则在这个序列中, i一定排在j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序
,不唯一,若网中所有顶点都在它的拓扑有序序列中,则该AOV 网必定不存在环。
public class CourseSchedule {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 邻接表存储图
List<Integer>[] graph = new ArrayList[numCourses];
for (int i = 0; i < numCourses; i++) {
graph[i] = new ArrayList<>();
}
int[] inDegrees = new int[numCourses]; // 记录各个顶点的入度
for (int[] pre : prerequisites) {
graph[pre[1]].add(pre[0]); // pre[1]为pre[0]的前提条件
inDegrees[pre[0]]++;
}
// 将入度为0的节点入队
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (inDegrees[i] == 0) {
queue.offer(i);
}
}
int count = 0; // 记录拓扑有序的节点的个数
while (!queue.isEmpty()) {
int currCourse = queue.poll();
count++;
for (int nextCourse : graph[currCourse]) {
if (--inDegrees[nextCourse] == 0) {
queue.offer(nextCourse);
}
}
}
return count == numCourses; // 拓扑有序的个数等于总个数,证明可以完成
}
public static void main(String[] args) {
int numCourses = 4;
int[][] prerequisites = {{1, 0}, {2, 0}, {3, 1}, {3, 2}};
CourseSchedule solution = new CourseSchedule();
boolean result = solution.canFinish(numCourses, prerequisites);
System.out.println("Can finish all courses: " + result);
}
}
4.8 图的关键路径
用一个有向图表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称为AOE网
(Activity On Edge) .
import java.util.*;
class Task {
int id; // 任务id
int duration; //
List<Task> predecessors; // 前置任务
List<Task> successors; // 后继任务
int earliestStartTime; // 最早开始时间
int latestStartTime; // 最晚开始时间
int earliestFinishTime; // 最早完成时间
int latestFinishTime; // 最晚完成时间
public Task(int id, int duration) {
this.id = id;
this.duration = duration;
this.predecessors = new ArrayList<>();
this.successors = new ArrayList<>();
}
}
public class CriticalPath {
public static List<Task> findCriticalPath(List<Task> tasks) {
List<Task> criticalPath = new ArrayList<>();
// 步骤1:计算每个任务的最早开始时间(ES)
calculateEarliestStartTime(tasks);
// 步骤2:计算每个任务的最晚开始时间(LS)和最早完成时间(EF)
calculateLatestStartTimeAndEarliestFinishTime(tasks);
// 步骤3:计算每个任务的总浮动时间,找出关键路径上的任务
for (Task task : tasks) {
int totalFloat = task.latestStartTime - task.earliestStartTime;
if (totalFloat == 0) {
criticalPath.add(task);
}
}
return criticalPath;
}
private static void calculateEarliestStartTime(List<Task> tasks) {
Queue<Task> queue = new LinkedList<>();
for (Task task : tasks) {
if (task.predecessors.isEmpty()) {
queue.add(task);
task.earliestStartTime = 0;
}
}
while (!queue.isEmpty()) {
Task currentTask = queue.poll();
int currentStartTime = currentTask.earliestStartTime;
for (Task successor : currentTask.successors) {
successor.predecessors.remove(currentTask);
successor.earliestStartTime = Math.max(successor.earliestStartTime, currentStartTime + currentTask.duration);
if (successor.predecessors.isEmpty()) {
queue.add(successor);
}
}
}
}
private static void calculateLatestStartTimeAndEarliestFinishTime(List<Task> tasks) {
Task lastTask = tasks.get(tasks.size() - 1);
lastTask.latestStartTime = lastTask.earliestStartTime;
lastTask.earliestFinishTime = lastTask.latestStartTime + lastTask.duration;
lastTask.latestFinishTime = lastTask.earliestFinishTime;
for (int i = tasks.size() - 2; i >= 0; i--) {
Task task = tasks.get(i);
int latestStartTime = Integer.MAX_VALUE;
for (Task successor : task.successors) {
latestStartTime = Math.min(latestStartTime, successor.latestStartTime - task.duration);
}
task.latestStartTime = latestStartTime;
task.earliestFinishTime = task.latestStartTime + task.duration;
task.latestFinishTime = task.earliestFinishTime;
}
}
public static void main(String[] args) {
Task A = new Task(1, 5);
Task B = new Task(2, 3);
Task C = new Task(3, 2);
Task D = new Task(4, 4);
Task E = new Task(5, 7);
Task F = new Task(6, 8);
A.successors.add(B);
A.successors.add(C);
B.successors.add(D);
C.successors.add(D);
D.successors.add(E);
E.successors.add(F);
List<Task> tasks = Arrays.asList(A, B, C, D, E, F);
List<Task> criticalPath = findCriticalPath(tasks);
System.out.println("Critical Path:");
for (Task task : criticalPath) {
System.out.println("Task " + task.id + ", Earliest Start Time: " + task.earliestStartTime
+ ", Latest Start Time: " + task.latestStartTime
+ ", Earliest Finish Time: " + task.earliestFinishTime
+ ", Latest Finish Time: " + task.latestFinishTime);
}
}
}
5. 查找算法
5.1 顺序查找(线性查找)
从线性表头查到尾即可。
//顺序查找
public int seqSerach(int[] array, int value) {
for (int i = 0; i < array.length; i++) {
if (array[i] == value) //若有多个value,则在这里先不返回,而是将下标添加到ArrayList中
return i;
}
return -1;
}
5.2 折半查找(二分查找)
仅适于有序的顺序表。每次都将要查找的值和中间的比较,若小则查在左边继续查找,若大则在右边继续查找。
//折半查找(升序序列)
public int binarySearch(int[] array, int value) {
int low = 0;
int high = array.length - 1;
int mid;
while (low <= high) {
mid = (low + high) / 2;
if (array[mid] == value) { //若要返回含有多个value值的下标,则用ArrayList添加,并在此if语句体中向左和右查找是否有相同的值
return mid;
} else if (array[mid] > value) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
5.3 插值查找(折半查找的改进)
折半查找的一种改进,即不每次用中间查找,而是mid=low+(value-array[low])*(high-low)/(array[high]-array[low]);,对于连续的有序序列查找比较方便,如查询1~100中的1。斐波那契查找是将mid= low+F[k-1]-1,数组长度不够的需补到和F[k]相等长度。
//插值查找(升序序列)
public int insertSearch(int[] array, int value) {
int low = 0;
int high = array.length - 1;
int mid;
while (low <= high) {
if (value < array[low] || value > array[high]) { //判断是否比最小值小或比最大值大
break;
}
mid = low + (value - array[low]) * (high - low) / (array[high] - array[low]); //就这个发生了改变
if (array[mid] == value) {
return mid;
} else if (array[mid] > value) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
5.4 分块查找
前一块中的最大数据,小于后一块中所有的数据(块内无序,块间有序);块数数量一般等于数字的个数开根号。查找时先确认要查找的数据属于哪一块,再从对应块中查找该数据。
class Block {
public int max; //块中最大值
public int startIndex;
public int endIndex;
public Block(int max, int startIndex, int endIndex) {
this.max = max;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
}
public class BlockSearch {
public static void main(String[] args) {
int[] arr = {16, 5, 9, 12, 21, 18,
32, 23, 37, 26, 45, 34,
50, 48, 61, 52, 73, 66};
Block block1 = new Block(21, 0, 5);
Block block2 = new Block(45, 6, 11);
Block block3 = new Block(73, 12, 17);
Block[] blockArr = {block1, block2, block3}; //定义索引表
int number = 34;
int index = getIndex(blockArr, arr, number);
System.out.println(index);
}
private static int getIndex(Block[] blockArr, int[] arr, int number) {
int block = getBlock(blockArr, number); //先查找在哪一个块
if (block == -1) { //该块不存在
return -1;
}
int startIndex = blockArr[block].startIndex;
int endIndex = blockArr[block].endIndex;
for (int i = startIndex; i <= endIndex; i++) { //再从块中查找
if (arr[i] == number) {
return i;
}
}
return -1;
}
private static int getBlock(Block[] blockArr, int number) {
for (int i = 0; i < blockArr.length; i++) {
if (number <= blockArr[i].max) {
return i;
}
}
return -1;
}
}
5.5 哈希查找(散列查找)
哈希表是一种数据结构:数据元素的关键字与其存储地址直接相关。可通过哈希函数(散列函数)将关键字映射到其对应的地址,常见的哈希函数如除留余数法(若哈希表表长为m,则取一个不大于m但最接近或等于m的质数p作为被除数,这样可以尽可能的减少冲突)。当不同关键字映射的值相同时(同义词),会发生冲突,可用拉链法(链地址法)
解决,即把所有的同义词存储在一个链表中。
//哈希查找
public boolean hashSearch(int[] array, int value) {
//先用拉链法将关键字根据哈希函数映射到对应的链表上
HashTable hashTable = new HashTable();
for (int j : array) {
hashTable.addData(j);
}
return hashTable.findData(value);
}
class HashTable {
LinkList[] hashTable = new LinkList[10]; //哈希表,存了10个链表
HashTable() {
for (int i = 0; i < hashTable.length; i++) {
hashTable[i] = new LinkList();
}
}
void addData(int key) {
hashTable[key % 10].add(key);
}
boolean findData(int key) {
return hashTable[key % 10].find(key);
}
}
class LinkList {
LinkedList<Integer> linkedList = new LinkedList<>(); //创建一个链表,这里可以换成一个对象,如员工,可根据id查询员工信息
void add(int key) {
linkedList.add(key);
}
boolean find(int key) {
return linkedList.contains(key);
}
}
6. 排序算法
内部排序:指将需要处理的所有数据都加载到内部存储器中进行排序。
外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
6.1 插入排序
6.1.1 直接插入排序O(n^2)
直接插入排序将数据依次按照有序的方式插入保证数据一直都保持有序。(当数据基本有序时效率较高,因此希尔排序就是在不断的让数据基本有序)
//直接插入排序(升序)
public void insertSort(int[] array) {
int len = array.length, i, j, temp;
//默认第0个元素是有序的,保证以后的插入也保持有序
for (i = 1; i < len; i++) {
if (array[i - 1] > array[i]) {
temp = array[i];
//从前边有序部分的最后往前进行比较,若要插入的数据比它小则把该数据后移
for (j = i - 1; j >= 0 && array[j] > temp; j--) {
array[j + 1] = array[j];
}
array[j + 1] = temp;
}
}
}
6.1.2 希尔排序O(n^1.3)
希尔排序先追求表中元素部分有序,再逐渐逼近全局有序。通过定义d增量,每间隔d的数据用直接插入排序,再不断缩小d进行一步步的排序。
//希尔排序(升序)
public void shellSort(int[] array) {
int len = array.length, i, j, temp;
for (int d = len / 2; d >= 1; d /= 2) { //步长不断减半
for (i = d; i < len; i++) {
if (array[i - d] > array[i]) { //直接判断有序的最后一个是否大于当前的,否就不用动已经有序了
temp = array[i]; //直接插入排序
for (j = i - d; j >= 0 && array[j] > temp; j -= d) {
array[j + d] = array[j];
}
array[j + d] = temp;
}
}
}
}
6.2 交换排序
6.2.1 冒泡排序O(n^2)
冒泡排序总共进行n-1轮次比较,每轮将对应的最大值固定,因此下一轮只需比较前边n-1-i个数据即可。
//冒泡排序(升序)
public void bubbleSort(int[] array) {
int len = array.length;
int temp;
for (int i = 0; i < len - 1; i++) { //总共n-1轮次就可将最大的n-1个元素确定
boolean flag = true; //若有一轮次没有发生交换,则证明已经有序,则可以停止
for (int j = 0; j < len - i - 1; j++) { //每一轮次只比较前n-i-1个没有确认顺序的数即可
//相邻的数进行比较,前边大则与后一个交换
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag = false;
}
}
if (flag) {
break;
}
}
}
6.2.2 快速排序O(nlog2n)
快速排序通过设置枢轴,让小于枢轴的移到左边,大于枢轴的移到右边,通过操作就可以固定枢轴的位置,再分别对枢轴的左边和右边进行重复的操作,直到都只剩一个元素。快排是平均性能最优的排序算法。优化:选取首、中、尾中间的元素作为枢轴;或随机选取枢轴。
//快速排序
public void quickSort(int[] array, int low, int high) {
if (low < high) {
int index = partition(array, low, high); //返回枢轴的位置
quickSort(array, low, index - 1);
quickSort(array, index + 1, high);
}
}
//快排的一次划分
public int partition(int[] array, int low, int high) {
int pivot = array[low]; //选取第一个元素作为枢轴
while (low < high) {
while (low < high && array[high] >= pivot)
high--;
array[low] = array[high];
while (low < high && array[low] <= pivot)
low++;
array[high] = array[low];
}
array[low] = pivot;
return low;
}
6.3 选择排序
6.3.1 简单选择排序O(n^2)
简单选择排序每次从数组中选取当前轮次最小值进行交换。
//简单选择排序(升序)
public void selectSort(int[] array) {
int len = array.length;
int temp;
for (int i = 0; i < len - 1; i++) {
int min = i; //默认第i个数据就是第i轮次的最小值
for (int j = i + 1; j < len; j++) {
if (array[j] < array[min]) {
min = j;
}
}
if (min != i) {
temp = array[min];
array[min] = array[i];
array[i] = temp;
}
}
}
6.3.2 堆排序O(nlog2n)
大根堆:根的值大于左右子结点的顺序存储的完全二叉树。构建方法:检查当前非叶子结点是否满足根>=左、右,若不满足,将当前结点与更大的一个孩子交换,同时还得判断更换之后是否还满足这种要求,若不满足则继续按同种规则进行交换。排序方法:将大根堆的根和待排序列中的最后一个元素进行交换,再重新构建堆,直到完全有序。堆的插入:先插在末尾,然后再和父节点依次往上进行对比;堆的删除:用堆最后的元素来代替要删除的元素,再和子节点依次往下进行对比,直到找到合适的位置。
//大根堆排序(升序)
public void maxHeapSort(int[] array) {
int len = array.length - 1;
int temp;
buildMaxHeap(array, len); //创建大根堆
print(array);
for (int i = len; i > 1; i--) {
temp = array[1];
array[1] = array[i];
array[i] = temp;
heapAdjust(array, 1, i - 1);
}
}
//建立大根堆(len是数组长度减一,由于第一个元素作哨兵了)
public void buildMaxHeap(int[] array, int len) {
for (int i = len / 2; i > 0; i--)
heapAdjust(array, i, len);
}
//将以根节点为k的树调整为大根堆
public void heapAdjust(int[] array, int k, int len) {
array[0] = array[k]; //第一个元素充当哨兵,若用这个元素,则不方便表示父节点和孩子结点的关系
for (int i = 2 * k; i <= len; i *= 2) {
if (i < len && array[i + 1] > array[i]) //取较大的孩子结点
i++;
if (array[0] > array[i]) //若比较大的孩子结点的值还大,则符合大根堆的定义
break;
else {
array[k] = array[i]; //将较大孩子结点移到父节点上
k = i; //修改k的值,现在根节点就变成了第i个结点
}
}
array[k] = array[0]; //找到最初根结点的位置
}
6.4 归并排序O(nlog2n)
归并排序是将两个或多个有序的序列合并成一个。
//归并排序
public void mergeSort(int[] array, int low, int high) {
if (low < high) {
int mid = (high + low) / 2;
mergeSort(array, low, mid); //左半部分归并
mergeSort(array, mid + 1, high); //右半部分归并
merge(array, low, mid, high);
}
}
//将两个有序序列合并([low,mid]与[mid+1,high])
public void merge(int[] array, int low, int mid, int high) {
int[] arrayFu = new int[array.length];
for (int i = low; i <= high; i++)
arrayFu[i] = array[i];
int i, j, k; //i指向前半部分序列,j指向后半部分序列,k指向合并之后应放的位置
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (arrayFu[i] <= arrayFu[j])
array[k] = arrayFu[i++];
else
array[k] = arrayFu[j++];
}
while (i <= mid)
array[k++] = arrayFu[i++];
while (j <= high)
array[k++] = arrayFu[j++];
}
6.5 基数排序O(d(n+r))
基数排序不是基于比较的排序算法。O(d(n+r)):d躺分配和收集,n数据个数,r基数的个数
基数排序得到递减序列的过程如下:
(1)初始化:设置r个空队列,Qr-1,Qr-2,…Qo,r指的是基数如十进制数的r为10
(2)按照各个关键字位权重递增的次序LSD(个、十、百),对d个关键字位分别做“分配”和“收集”
(3)分配︰顺序扫描各个元素,若当前处理的关键字位=x,则将元素插入Qx队尾
(4)收集:把Qr-1,Qr-2,…,Qo各个队列中的结点依次出队并链接(按照Qo,Q1,…,Qr-1顺序出队并链接就得到递增序列)
//8.基数排序(升序,只支持正整数)
public void radixSort(int[] array) {
LinkedList[] lists = new LinkedList[10];//创建10个链式队列
for (int i = 0; i < lists.length; i++) {
lists[i] = new LinkedList();
}
int len = array.length;
int max = array[0];
for (int j : array) { //求数组的最大值
if (j > max) {
max = j;
}
}
int k = (int) Math.log(max) + 1; //求出最大值的位数
for (int j = 0; j < k; j++) {
for (int i = 0; i < len; i++) { //分配
lists[getNum(array[i], j)].add(array[i]);
}
int index = 0;
for (int i = 0; i < 10; i++) { //收集
while (lists[i].peek() != null) {
array[index++] = (int) lists[i].remove();
}
}
}
}
//定义一个函数求十位、个位、百位的值
public int getNum(int number, int index) {
if (index == 0) {
return number % 10;
}
return getNum(number / 10, index - 1);
}
6.6 外部排序
外部排序是用的归并排序来实现的(2路归并:2个输入缓冲区和1个输出缓冲区,可多路归并提升速度)。
(1)内存先读取外存磁盘数据到输入缓冲区1和输入缓冲区2,通过归并排序将这两个输入缓冲区排序并写到外存磁盘。
(2)外存现在是两块一组都是有序的,再将两块通过归并变成四块有序的,首先分别读取两组两块的第一块到输入缓冲区1和输入缓冲区2,根据归并排序规则不断到输出缓冲区,若输出缓冲区满了就写入外存,若有输入缓冲区空了则继续读取两块一组的第二块数组,继续归并排序。
外部排序时间开销 = 读写外存的时间 + 内部排序所需时间 + 内部归并所需时间
7. 常用算法思想
7.1 递归与回溯
递归就是方法自己调用自己,必须有递归出口,代码简洁但效率低下,递归调用过程是用独立的栈(局部变量都是独立的)来实现。递归可以把大规模的问题不断变小,再进行推导。
回溯就是一步步向前试探,对每一步探测的情况评估,再决定是否继续,可避免走弯路。当出现非法的情况时,可退到之前的情景,可返回一步或多步,再去尝试别的路径和办法。回溯利用递归的性质不断的尝试。
(迷宫、八皇后、汉诺塔、球和篮子)
7.1.1 迷宫回溯
可用递归实现迷宫回溯,按照不同的搜索策略(下右上左、上右下左)可能会导致路径长度的不同。
import java.util.Random;
public class MiGong {
public static void main(String[] args) {
//创建二维数组来模拟迷宫
int[][] map = new int[10][10];
Random random = new Random();
//值1代表墙,值0代表路,四周置为墙,并随机设置一些挡板
for (int i = 0; i < 10; i++) {
map[0][i] = 1;
map[9][i] = 1;
map[i][0] = 1;
map[i][9] = 1;
map[random.nextInt(8) + 1][random.nextInt(8) + 1] = 1;
}
map[1][1] = 0; //起始位置
map[8][8] = 0; //终点位置
findWay(map, 1, 1, 8, 8);
print(map);
}
//从(startI,startJ)位置开始到(endI,endJ)是否有路,0:没有走过,1:墙,2:通路可走,3:走过但路不通
//走迷宫时按照下->右->上->左的顺序进行搜索式前进,如果都走不通,就回溯到上一个结点
public static boolean findWay(int[][] map, int startI, int startJ, int endI, int endJ) {
if (map[endI][endJ] == 2) { //递归出口
return true;
} else {
if (map[startI][startJ] == 0) { //如果该点还没有走过
map[startI][startJ] = 2; //假定该点可以走通
if (findWay(map, startI + 1, startJ, endI, endJ)) { //如果向下走可走通
return true;
} else if (findWay(map, startI, startJ + 1, endI, endJ)) { //如果向右可以走通
return true;
} else if (findWay(map, startI - 1, startJ, endI, endJ)) { //如果向上可以走通
return true;
} else if (findWay(map, startI, startJ - 1, endI, endJ)) { //如果向左可以走通
return true;
} else { //全都走不通则就要回溯,并置为3说明该点走过但是不通
map[startI][startJ] = 3;
return false;
}
} else { //1表示墙,指定不可能;2表示通路;3表示走过但不通,所以只要不为0其它值都表示走不通
return false;
}
}
}
//打印地图
public static void print(int[][] map) {
for (int[] ints : map) {
for (int anInt : ints) {
System.out.print(anInt + " ");
}
System.out.println();
}
}
}
7.1.2 八皇后回溯
任意两个皇后都不能处在同一行、同一列或同一斜线上。解析:当放置第K+1个皇后时代表前K个皇后位置不冲突符合要求,因此跳出递归,但是放置第K个皇后所在列数的for循环还没有结束,除非到最后一列才结束,故此是有回溯的。
public class KQueens {
public int k;
public int[] array; //存放皇后的顺序,int[i] = val表示第i+1行的皇后放在了第val+1列
public int count;
public KQueens(int k) {
this.k = k;
this.array = new int[k];
this.count = 0;
}
public static void main(String[] args) {
KQueens kQueens = new KQueens(8);
kQueens.check(0);
System.out.println("共有" + kQueens.count + "种解法");
}
//放置第n个皇后,由于每次冲突都会继续判断下一列,因此有回溯
private void check(int n) {
if (n == k) { //当n等于k的时候代表已经要放置第k+1个皇后了,证明前边k个皇后符合要求
print();
count++;
return;
}
//依次放入皇后
for (int i = 0; i < k; i++) {
//先把第n个皇后放入该行的第一列
array[n] = i;
if (isRational(n)) { //第n个皇后不冲突就放第n+1个皇后
check(n + 1);
}
//若冲突则继续放下一列再判断是否冲突
}
}
//查看第n个皇后是否与之前n-1个皇后的摆放位置是否冲突
private boolean isRational(int n) {
for (int i = 0; i < n; i++) {
//当处于同一列、下标差值(行距)等于列差值(列距)即斜率为1,则不合理
if (array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i])) {
return false;
}
}
return true;
}
//打印正确的皇后摆放位置
private void print() {
for (int j : array) {
System.out.print(j + " ");
}
System.out.println();
}
}