数据结构
数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。
数据结构包括:线性结构、非线性结构。
线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性存储关系。线性结构有两种不同的存储结构,即顺序存储和链式存储。顺序存储的线性表被称为顺序表,顺序表中存储元素的地址是连续的;链式存储的线性表被称为链表,链表中存储元素的地址不一定是连续的,元素结点中存放数据元素以及相邻元素地址信息。
常见的线性结构有:数组、链表、队列、栈;常见非线性结构有:多维数组、广义表、树结构、图结构。
稀疏数组
使用稀疏数组可以用来压缩数据。稀疏数组的第一行依次记录原数组一共有几行几列,有多少个不为零的值,之后的每行记录原数组中不为零的值所在的行数、列数以及数组中元素该值。如图所示:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VoowXJhg-1664240469534)(/iblog/posts/annex/images/essays/数据结构与算法-001.png)]](https://img-blog.csdnimg.cn/d7f35b8655964dd0a6446b49d82d6b76.png)
二维数组转稀疏数组
class TwoDimensionArrayDemo {
public static int[][] twoDimensionArrayToSparseArray(int[][] array) {
int sum = 0;
int row = array.length;
int column = 0;
for (int[] ints : array) {
column = ints.length;
for (int item : ints) {
if (item != 0) {
sum++;
}
}
}
int[][] sparseArray = new int[sum + 1][3];
sparseArray[0][0] = row;
sparseArray[0][1] = column;
sparseArray[0][2] = sum;
int count = 0;
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array[i].length; j++) {
if (array[i][j] != 0) {
count++;
sparseArray[count][0] = i;
sparseArray[count][1] = j;
sparseArray[count][2] = array[i][j];
}
}
}
System.out.println("稀疏数组====》");
for (int i = 0; i < sparseArray.length; i++) {
System.out.printf("%d\t%d\t%d\t\n", sparseArray[i][0], sparseArray[i][1], sparseArray[i][2]);
}
return sparseArray;
}
}
稀疏数组转二维数组
class TwoDimensionArrayDemo {
public static int[][] sparseArrayToTwoDimensionArray(int[][] sparseArray) {
int[][] toTwoDimensionArray = new int[sparseArray[0][0]][sparseArray[0][1]];
for (int i = 1; i < sparseArray.length; i++) {
toTwoDimensionArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
System.out.println("二维数组====》");
for (int[] row : toTwoDimensionArray) {
for (int data : row) {
System.out.printf("%d\t", data);
}
System.out.println();
}
return toTwoDimensionArray;
}
}
队列
队列是一个有序列表,可以使用数组或链表来实现。队列遵循先入先出的原则。即,先存入队列的数据,要先取出。后存入的要后取出。
使用数组模拟队列示意图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qh6FB5Ho-1664240469535)(/iblog/posts/annex/images/essays/数据结构与算法-002.png)]](https://img-blog.csdnimg.cn/a32e226ffd7746609e4dd8c26d7b1fd3.png)
数组模拟单向队列
public class ArrayQueue{
private int capacity;
private int[] arr;
private int front;
private int rear;
public ArrayQueue(int capacity) {
this.capacity = capacity;
arr = new int[capacity];
front = -1;
rear = -1;
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return capacity - 1 == rear;
}
public void add(int data) {
if (isFull()) {
System.out.println("队列已经满了,不能在继续添加");
return;
}
arr[++rear] = data;
}
public int get() {
if (isEmpty()) {
System.out.println("队列为空,不能获取元素");
return -1;
}
return arr[++front];
}
public void show() {
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
System.out.println("开始遍历队列:");
for (int i = front + 1; i <= rear; i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front + 1];
}
}
数组模拟环形队列
public class CircleArrayQueue{
private int capacity;
private int[] arr;
private int front;
private int rear;
public CircleArrayQueue(int capacity) {
this.capacity = capacity;
arr = new int[capacity];
}
public boolean isEmpty() {
return front == rear;
}
public boolean isFull() {
return (rear + 1) % capacity == front;
}
public void add(int data) {
if (isFull()) {
System.out.println("队列已经满了,不能在继续添加");
return;
}
arr[rear] = data;
rear = (rear + 1) % capacity;
}
public int get() {
if (isEmpty()) {
System.out.println("队列为空,不能获取元素");
return -1;
}
int value = arr[front];
front = (front + 1) % capacity;
return value;
}
public void show() {
if (isEmpty()) {
System.out.println("队列空的,没有数据~~");
return;
}
System.out.println("开始遍历队列:");
for (int i = front % capacity; i < front + ((rear + capacity - front) % capacity); i++) {
System.out.printf("arr[%d]=%d\n", i, arr[i]);
}
}
public int peek() {
if (isEmpty()) {
throw new RuntimeException("队列空的,没有数据~~");
}
return arr[front];
}
}
链表
链表属于线性结构,存储空间不连续。
链表特点:
- 链表是以节点的方式来存储,是链式存储;data 域存放数据,next 域指向下一个节点;
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OOgL9dyS-1664240469536)(/iblog/posts/annex/images/essays/数据结构与算法-003.png)]](https://img-blog.csdnimg.cn/d6d0bcee37a3448b9091e34484c08288.png)
单向链表
操作单向链表:对于插入、删除操作,只能定位至待操作节点的前一个节点,如果定位至当前节点,那么其上一个节点的信息便会丢失。
单向链表,链表的增、删、查、改
class SingleLinkedList{
private Node headNode = new Node(0,"");
public void add(Node node){
Node tmpNode = headNode;
while (tmpNode.next != null){
tmpNode = tmpNode.next;
}
tmpNode.next = node;
}
public void addByOrder(Node node){
boolean flag = false;
Node tmp = headNode;
while (true){
if (tmp.next == null) {
break;
}
if (node.num < tmp.next.num){
break;
}
if (node.num == tmp.next.num){
flag = true;
break;
}
tmp = tmp.next;
}
if (!flag){
node.next = tmp.next;
tmp.next = node;
return;
}
System.out.printf("需要添加的结点编号:%d已经存在了",node.num);
}
public void list() {
Node tmpNode = headNode.next;
if (tmpNode == null){
System.out.println("链表为空!");
return;
}
while (tmpNode != null){
System.out.println(tmpNode);
tmpNode = tmpNode.next;
}
}
}
class Node {
int num;
String name;
Node next;
public Node(int num,String name){
this.num = num;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"num=" + num +
", name='" + name + '\'' +
'}';
}
}
反转单向链表
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sXalBJpA-1664240469537)(/iblog/posts/annex/images/essays/数据结构与算法-004.png)]](https://img-blog.csdnimg.cn/75879482710d475e89682b3423905c59.png)
class LinkedListDemo{
public void reserve(Node head) {
if (head.next == null || head.next.next == null) {
return;
}
Node reserve = new Node(0, "");
Node cur = head.next;
Node next = null;
while (cur != null) {
next = cur.next;
cur.next = reserve.next;
reserve.next = cur;
cur = next;
}
}
}
利用栈逆序打印单向链表
class LinkedListDemo {
public void reservePrint(Node head) {
if (head.next == null || head.next.next == null) {
return;
}
Stack<Node> nodes = new Stack<>();
Node tmp = head.next;
while (tmp != null) {
nodes.push(tmp);
tmp = tmp.next;
}
while (nodes.size() > 0) {
System.out.println(nodes.pop());
}
}
}
双向链表
对比单向链表:
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找;
- 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2vhRzAjB-1664240469537)(/iblog/posts/annex/images/essays/数据结构与算法-005.png)]](https://img-blog.csdnimg.cn/c22f44675db94a70a94292e6af56bc6f.png)
双向链表,增、删、改、查
class DoubleLinkedList{
private Node headNode = new Node(0,"");
public Node getHeadNode() {
return headNode;
}
public void add(Node node){
Node tmpNode = headNode;
while (tmpNode.next != null){
tmpNode = tmpNode.next;
}
tmpNode.next = node;
node.prev = tmpNode;
}
public void update(Node node){
if (headNode == null) {
return;
}
Node tmp = headNode.next;
while (true){
if (tmp == null){
break;
}
if (node.num == tmp.num){
tmp.name = node.name;
break;
}
tmp = tmp.next;
}
}
public void remove(int num){
if (headNode.next == null){
System.out.println("链表为空,无法删除!");
return;
}
Node tmp = headNode.next;
while (tmp != null){
if (num == tmp.num){
tmp.prev.next = tmp.next;
if(tmp.next != null) {
tmp.next.prev = tmp.prev;
}
break;
}
tmp = tmp.next;
}
}
public void list() {
Node tmpNode = headNode.next;
if (tmpNode == null){
System.out.println("链表为空!");
return;
}
while (tmpNode != null){
System.out.println(tmpNode);
tmpNode = tmpNode.next;
}
}
}
class Node {
int num;
String name;
Node next;
Node prev;
public Node(int num,String name){
this.num = num;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"num=" + num +
", name='" + name + '\'' +
'}';
}
}
约瑟夫问题
约瑟夫问题为:设编号为 1,2,…n的n个人围坐一圈,约定编号为 k(1<=k<=n) 的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列, 依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
用一个不带头结点的循环链表来处理约瑟夫问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKXdjczy-1664240469539)(/iblog/posts/annex/images/essays/数据结构与算法-006.png)]](https://img-blog.csdnimg.cn/81257773dd95488bb7afb587713956cc.png)
class JoesphSingletonLinkedList {
private Node first = null;
public void add(int nums) {
if (nums < 1) {
System.out.println("nums的值不正确");
return;
}
Node cur = null;
for (int i = 1; i <= nums; i++) {
Node node = new Node(i);
if (i == 1) {
first = node;
first.next = first;
cur = first;
} else {
cur.next = node;
node.next = first;
cur = node;
}
}
}
public void list() {
Node tmp = first;
while (true){
System.out.printf("当前结点为:%d\n",tmp.num);
if (tmp.next == first){
break;
}
tmp = tmp.next;
}
}
public void joseph(int startNum,int countNum,int sum){
if (startNum > sum || startNum < 0 || countNum < 0) {
System.out.println("输入的参数不正确!");
return;
}
Node helper = first;
while (helper.next != first) {
helper = helper.next;
}
for (int i = 0; i < startNum - 1; i++) {
first = first.next;
helper = helper.next;
}
while (true){
if (first == helper){
break;
}
for (int i = 0; i < countNum - 1; i++) {
first = first.next;
helper = helper.next;
}
System.out.printf("当前出队列的结点编号为:%d\n",first.num);
first = first.next;
helper.next = first;
}
System.out.printf("最后的结点为:%d\n",first.num);
}
}
class Node {
int num;
Node next;
public Node(int num){
this.num = num;
}
@Override
public String toString() {
return "Node{" +
"num=" + num +
'}';
}
}
栈
栈的英文为stack,是一个先入后出(FILO-First In Last Out)的有序列表,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。
栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
下图是出栈和入栈
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQgLTWeO-1664240469541)(/iblog/posts/annex/images/essays/数据结构与算法-007.png)]](https://img-blog.csdnimg.cn/1c5f10f37b62471b9451f22372f03d09.png)
栈的应用场景:
- 子程序的调用: 在跳往子程序前, 会先将下个指令的地址存到堆栈中, 直到子程序执行完后再将地址取出, 以回到原来的程序中;
- 处理递归调用: 和子程序的调用类似, 只是除了储存下一个指令的地址外, 也将参数、 区域变量等数据存入栈中;
- 表达式的转换:中缀表达式转后缀表达式与求值;
- 二叉树的遍历;
- 图形的深度优先(depth 一 first)搜索法;
数组模拟栈
class ArrayStack<T>{
private int size;
private int top;
private Object[] stack;
public ArrayStack(int size) {
this.size = size;
this.stack = new String[size];
top = -1;
}
public boolean isFull(){
return top == size -1;
}
public boolean isEmpty() {
return top == -1;
}
public void push(T item) {
if (isFull()) {
System.out.println("栈已经满了,不能继续添加!");
return;
}
top++;
this.stack[top] = item;
}
public T pop() {
if (isEmpty()) {
throw new RuntimeException("栈已经为空,不能继续pop");
}
T val = (T)this.stack[top];
top--;
return val;
}
public void list() {
if (isEmpty()) {
System.out.println("栈为空不能继续遍历!");
return;
}
System.out.println("遍历栈==》");
for (int i = top; i >=0; i--){
System.out.println(this.stack[i]);
}
}
}
栈实现中缀表达式
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lAb0rcmh-1664240469542)(/iblog/posts/annex/images/essays/数据结构与算法-008.png)]](https://img-blog.csdnimg.cn/f881250b81f146cf9e84473f0635997b.png)
class CalcInfixExpressions {
public int calcInfixExpressions(String expression) {
char[] chars = expression.toCharArray();
int len = chars.length;
Stack<Integer> numStack = new Stack<>();
Stack<Character> oprStack = new Stack<>();
int index = 0;
for (int j = 0; j < len; j++) {
char ch = chars[j];
index++;
if (Character.isDigit(ch)) {
int num = ch;
boolean flag = false;
for (int i = index; i < len; i++) {
if (Character.isDigit(expression.charAt(i))) {
String strNum = String.valueOf(ch) + expression.charAt(i);
num = Integer.parseInt(strNum);
flag = true;
index++;
j++;
}else {
break;
}
}
if (!flag) {
num -= 48;
}
numStack.push(num);
continue;
}
if (oprStack.isEmpty()) {
oprStack.push(ch);
continue;
}
if (oprPriority(oprStack.peek()) >= oprPriority(ch)) {
numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
}
oprStack.push(ch);
}
while (!oprStack.isEmpty()){
numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
}
return numStack.pop();
}
private int oprPriority(int ch) {
if (ch == '*' || ch == '/') {
return 2;
}
if (ch == '+' || ch == '-') {
return 1;
}
return -1;
}
private int calc(int num1, int num2, int opr) {
int res = 0;
switch (opr) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
}
return res;
}
}
栈实现后缀表达式
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
思路:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。
class PolandNotation{
public int calcSuffixExceptions(String suffixExpres) {
char[] chars = suffixExpres.toCharArray();
Stack<Integer> stack = new Stack<>();
int res =0;
for (int i = 0; i < chars.length; i++) {
char ch = suffixExpres.charAt(i);
if (!Character.isDigit(ch)) {
res = calc(stack.pop(),stack.pop(),ch);
stack.push(res);
}else {
stack.push(ch - 48);
}
}
return res;
}
private int calc(int num1, int num2, int opr) {
int res = 0;
switch (opr) {
case '+':
res = num1 + num2;
break;
case '-':
res = num2 - num1;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num2 / num1;
break;
default:
throw new RuntimeException("运算符有误");
}
return res;
}
}
中缀表达式转后缀表达式
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2uSHvqN-1664240469544)(/iblog/posts/annex/images/essays/数据结构与算法-009.png)]](https://img-blog.csdnimg.cn/b028e0c52f46403eb3e75427c95748a1.png)
中缀表达式转后缀表达式代码实现
class InfixToPolandNotation{
private boolean isNumber(char ch){
return ch >=48 && ch <= 57;
}
private int oprPriority(String opr) {
if (opr.equals("*") || opr.equals("/")) {
return 2;
}
if (opr.equals("+") || opr.equals("-")) {
return 2;
}
return -1;
}
public List<String> toInfixExceptionList(String str) {
ArrayList<String> list = new ArrayList<>();
int index = 0;
StringBuilder number;
char c;
while (index < str.length()){
if (!isNumber((c = str.charAt(index)))){
list.add(String.valueOf(c));
index++;
}else {
number = new StringBuilder();
while (index < str.length() && isNumber((c = str.charAt(index)))){
index++;
number.append(c);
}
list.add(number.toString());
}
}
return list;
}
public List<String> infixExpressionToSuffixExpress(List<String> list) {
Stack<String> stack = new Stack<>();
ArrayList<String> finalList = new ArrayList<>();
for (String item : list) {
if (item.matches("\\d+")){
finalList.add(item);
continue;
}
if (item.equals("(")){
stack.push(item);
continue;
}
if (item.equals(")")){
while (!stack.peek().equals("(")) {
finalList.add(stack.pop());
}
stack.pop();
}else {
while (stack.size() > 0 && oprPriority(stack.peek()) >= oprPriority(item)){
finalList.add(stack.pop());
}
stack.push(item);
}
}
while (stack.size() != 0) {
finalList.add(stack.pop());
}
return finalList;
}
}
完整逆波兰表达式代码,支持小数、支持消除空格
public class ReversePolishMultiCalc {
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
static final int LEVEL_01 = 1;
static final int LEVEL_02 = 2;
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
public static String replaceAllBlank(String s ){
return s.replaceAll("\\s+","");
}
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ;
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
哈希表
哈希表也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。
它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
手写模拟哈希表
public class HashTableDemo {
public static void main(String[] args) {
HashTab hashTab = new HashTab(3);
Node node1 = new Node(1, "zs");
Node node2 = new Node(2, "lx");
Node node3 = new Node(3, "ex");
Node node4 = new Node(4, "as");
Node node5 = new Node(7, "we");
hashTab.put(node1);
hashTab.put(node2);
hashTab.put(node3);
hashTab.put(node4);
hashTab.put(node5);
System.out.println("添加元素后===》");
System.out.println(hashTab.toString());
System.out.println("删除后===》");
hashTab.remove(4);
System.out.println(hashTab.toString());
}
}
class HashTab {
private NodeList[] nodes;
private int size;
public HashTab(int size) {
this.size = size;
nodes = new NodeList[size];
for (int i = 0; i < size; i++) {
nodes[i] = new NodeList();
}
}
public void put(Node node) {
nodes[getPosition(node.id)].add(node);
}
public void remove(int id) {
nodes[getPosition(id)].delete(id);
}
private int getPosition(int id) {
return id % size;
}
@Override
public String toString() {
return "HashTab{" +
"nodes=\n" + Arrays.toString(nodes) +
"}";
}
}
class NodeList {
Node head = null;
public void add(Node node) {
if (head == null) {
head = node;
return;
}
Node tmp = head;
while (tmp.next != null) {
tmp = tmp.next;
}
tmp.next = node;
}
public void list() {
if (head == null) {
System.out.println("当前链表为空");
return;
}
Node tmp = head;
while (true) {
System.out.println(tmp);
if (tmp.next == null) {
break;
}
tmp = tmp.next;
}
}
public void delete(int id) {
if (head == null) {
System.out.println("当前链表为空");
return;
}
if (head.id == id) {
head = head.next;
return;
}
Node preNode = head;
Node curNode = preNode.next;
while (curNode != null) {
if (curNode.id == id) {
preNode.next = curNode.next;
System.out.println("删除成功,删除的是: " + curNode.id + "," + curNode.name);
curNode = null;
return;
}
preNode = preNode.next;
curNode = curNode.next;
}
System.out.println("删除失败,节点不存在");
}
@Override
public String toString() {
return "NodeList{" +
"head=" + head +
"}\n";
}
}
class Node {
int id;
String name;
Node next;
public Node(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", name='" + name + '\'' +
", next=" + next +
"}";
}
}
二叉树
二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
如果该二叉树的所有叶子节点都在最后一层,并且结点总数为 2^n -1, n 为层数,则称为满二叉树。
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
PS: 二叉树中结点等价于节点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyH38uZc-1664240469545)(/iblog/posts/annex/images/essays/数据结构与算法-022.png)]](https://img-blog.csdnimg.cn/397ecd4fe2c34c3fa58577178a073d1a.png)
二叉树的遍历及查找
对于二叉树来讲最主要、最基本的运算是遍历。遍历二叉树 是指以一定的次序访问二叉树中的每个结点。所谓访问结点是指对结点进行各种操作的简称。
例如,查询结点数据域的内容,或输出它的值,或找出结点位置,或是执行对结点的其他操作。遍历二叉树的过程实质是把二叉树的结点进行线性排列的过程。
二叉树的遍历方法:
- 前序遍历:先访问根结点,然后前序遍历左子树,在前序遍历右子树;
- 中序遍历:中序遍历根结点的左子树,然后访问根结点,最后遍历右子树;
- 后序遍历:后序遍历左子树,然后后序遍历右子树,最后访问根结点;
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTreeObj root = new BinaryTreeObj(1,"zs");
BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
root.setLeft(binaryTreeObj2);
root.setRight(binaryTreeObj3);
binaryTreeObj3.setLeft(binaryTreeObj4);
binaryTreeObj3.setRight(binaryTreeObj5);
BinaryTree binaryTree = new BinaryTree();
binaryTree.setRoot(root);
binaryTree.preOrderShow();
BinaryTreeObj binaryTreeObj = binaryTree.preOrderSearch(11);
if (binaryTreeObj == null) {
System.out.println("没有找到该结点~");
return;
}
System.out.printf("找到当前结点:id: %d, name: %s",binaryTreeObj.getId(),binaryTreeObj.getName());
}
}
class BinaryTree {
private BinaryTreeObj root;
public void setRoot(BinaryTreeObj root) {
this.root = root;
}
public void preOrderShow() {
if (this.root != null) {
this.root.preOrder();
}
}
public void postOrderShow() {
if (this.root != null) {
this.root.postOrder();
}
}
public void midOrderShow() {
if (this.root != null) {
this.root.midOrder();
}
}
public BinaryTreeObj preOrderSearch(int id){
if (this.root != null) {
return this.root.preOrderSearch(id);
}
return null;
}
public BinaryTreeObj infixOrderSearch(int id){
if (this.root != null) {
return this.root.infixOrderSearch(id);
}
return null;
}
public BinaryTreeObj postOrderSearch(int id){
if (this.root != null) {
return this.root.postOrderSearch(id);
}
return null;
}
}
class BinaryTreeObj {
private Integer id;
private String name;
public BinaryTreeObj(Integer id,String name){
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public Integer getId() {
return id;
}
private BinaryTreeObj left;
private BinaryTreeObj right;
public void setLeft(BinaryTreeObj left) {
this.left = left;
}
public void setRight(BinaryTreeObj right) {
this.right = right;
}
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
public void postOrder(){
if (this.left != null){
this.left.postOrder();
}
if (this.right != null){
this.right.postOrder();
}
System.out.println(this);
}
public void midOrder(){
if (this.left != null){
this.left.midOrder();
}
System.out.println(this);
if (this.right != null){
this.right.midOrder();
}
}
public BinaryTreeObj preOrderSearch(int id) {
if (id == this.id){
return this;
}
BinaryTreeObj binaryTreeObj = null;
if (this.left != null){
binaryTreeObj = this.left.preOrderSearch(id);
}
if (binaryTreeObj != null){
return binaryTreeObj;
}
if (this.right != null){
binaryTreeObj = this.right.preOrderSearch(id);
}
return binaryTreeObj;
}
public BinaryTreeObj infixOrderSearch(int id) {
BinaryTreeObj binaryTreeObj = null;
if (this.left != null){
binaryTreeObj = this.left.infixOrderSearch(id);
}
if (id == this.id){
return this;
}
if (binaryTreeObj != null){
return binaryTreeObj;
}
if (this.right != null){
binaryTreeObj = this.right.infixOrderSearch(id);
}
return binaryTreeObj;
}
public BinaryTreeObj postOrderSearch(int id) {
BinaryTreeObj binaryTreeObj = null;
if (this.left != null){
binaryTreeObj = this.left.postOrderSearch(id);
}
if (binaryTreeObj != null){
return binaryTreeObj;
}
if (this.right != null){
binaryTreeObj = this.right.postOrderSearch(id);
}
if (id == this.id){
return this;
}
return binaryTreeObj;
}
@Override
public String toString() {
return "BinaryTreeObj{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
二叉树的删除
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ADz0A7z1-1664240469546)(/iblog/posts/annex/images/essays/数据结构与算法-023.png)]](https://img-blog.csdnimg.cn/09cb4ce4d00f4aaeacd854177dbeb863.png)
class BinaryTree {
public void del(int id){
if (this.root == null){
return;
}
if (this.root.getId() == id){
this.root = null;
return;
}
this.root.delNo(id);
}
}
class BinaryTreeObj {
public void delNo(int id){
if (this.left != null && this.left.id == id){
this.left = null;
return;
}
if (this.right != null && this.right.id == id){
this.right = null;
return;
}
if (this.left == null && this.right == null){
return;
}
if (this.left != null){
this.left.delNo(id);
}
if (this.right != null) {
this.right.delNo(id);
}
}
}
顺序存储二叉树
二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。
需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。
顺序存储二叉树应用实例:八大排序算法中的堆排序,就会使用到顺序存储二叉树。
public class ArrBinaryTreeDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7};
ArrBinaryTree arrBinaryTree = new ArrBinaryTree();
arrBinaryTree.setArrayTree(arr);
arrBinaryTree.preArrBinaryTree(0);
}
}
class ArrBinaryTree {
private int[] arr = null;
public void setArrayTree(int[] arr) {
this.arr = arr;
}
public void preArrBinaryTree(int index) {
if (arr == null || arr.length == 0) {
return;
}
System.out.println(arr[index]);
int nextLeftIndex = (index << 1) + 1;
int nextRightIndex = (index << 1) + 2;
if (nextLeftIndex < arr.length) {
preArrBinaryTree(nextLeftIndex);
}
if (nextRightIndex < arr.length) {
preArrBinaryTree(nextRightIndex);
}
}
}
线索化二叉树
n 个结点的二叉链表中含有 n+1【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在 某种遍历次序 下的前驱和后继结点的指针,这种附加的指针称为"线索"。这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。
根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
如图,按照前序遍历可以得到数组 {1, 2, 4, 5, 3, 6} 其中
- 【2】的前驱结点为【1】,后继结点为【4】
- 【6】的前驱结点为【3】,后继结点为 null
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zD0UyPKA-1664240469547)(/iblog/posts/annex/images/essays/数据结构与算法-024.png)]](https://img-blog.csdnimg.cn/1891443ace364750af55a88f648e9199.png)
中序线索化二叉树示意图
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3sw2IRl-1664240469549)(/iblog/posts/annex/images/essays/数据结构与算法-025.png)]](https://img-blog.csdnimg.cn/399f05faa6e54efeb30633c1e33085c6.png)
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
BinaryTreeObj root = new BinaryTreeObj(1,"zs");
BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
root.setLeft(binaryTreeObj2);
root.setRight(binaryTreeObj3);
binaryTreeObj3.setLeft(binaryTreeObj4);
binaryTreeObj3.setRight(binaryTreeObj5);
ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
threadedBinaryTree.infixThreadedBinaryTree(root);
threadedBinaryTree.forEachInfixThreadedBinaryTree(root);
}
}
class ThreadedBinaryTree{
private BinaryTreeObj pre;
public void infixThreadedBinaryTree(BinaryTreeObj node){
if (node == null){
return;
}
infixThreadedBinaryTree(node.getLeft());
if (node.getLeft() == null){
node.setLeft(pre);
node.setLeftType(1);
}
if (pre != null && pre.getRight() == null) {
pre.setRight(node);
pre.setRightType(1);
}
pre = node;
infixThreadedBinaryTree(node.getRight());
}
public void forEachInfixThreadedBinaryTree(BinaryTreeObj root) {
BinaryTreeObj node = root;
while (node != null){
while (node.getLeftType() == 0){
node = node.getLeft();
}
System.out.println(node);
while (node.getRightType() == 1){
node = node.getRight();
System.out.println(node);
}
node = node.getRight();
}
}
}
class BinaryTreeObj {
private Integer id;
private String name;
private BinaryTreeObj left;
private BinaryTreeObj right;
private int leftType;
private int rightType;
public BinaryTreeObj(Integer id,String name){
this.id = id;
this.name = name;
}
public String getName() {
return name;
}
public Integer getId() {
return id;
}
public BinaryTreeObj getLeft() {
return left;
}
public BinaryTreeObj getRight() {
return right;
}
public void setLeft(BinaryTreeObj left) {
this.left = left;
}
public void setRight(BinaryTreeObj right) {
this.right = right;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
public int getLeftType() {
return leftType;
}
public int getRightType() {
return rightType;
}
}
哈夫曼树
哈夫曼树重要概念:
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1;
- 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积;
- 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。WPL 最小的就是赫夫曼树;
给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,称为哈夫曼树,有些资料也译为赫夫曼树。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xx2IzuGm-1664240469549)(/iblog/posts/annex/images/essays/数据结构与算法-028.png)]](https://img-blog.csdnimg.cn/c591735d9b724766b50892b8a1478031.png)
WPL最小的就是哈夫曼树,如上图,中间的树就是哈夫曼树。
public class HuffmanTreeDemo {
public static void main(String[] args) {
HuffmanTree huffmanTree = new HuffmanTree();
HuffmanTreeNode root = huffmanTree.buildHuffmanTree(new int[]{13, 7, 8, 3, 29, 6, 1});
System.out.println("前序遍历huffman树:");
huffmanTree.preOrder(root);
}
}
class HuffmanTree{
public void preOrder(HuffmanTreeNode root){
if (root != null){
root.preOrder();
}
}
public HuffmanTreeNode buildHuffmanTree(int[] arr){
List<HuffmanTreeNode> list = new ArrayList<>();
for (int value : arr) {
list.add(new HuffmanTreeNode(value));
}
while (list.size() > 1){
Collections.sort(list);
HuffmanTreeNode leftNode = list.get(0);
HuffmanTreeNode rightNode = list.get(1);
HuffmanTreeNode parentNode = new HuffmanTreeNode(leftNode.value + rightNode.value);
parentNode.left = leftNode;
parentNode.right = rightNode;
list.remove(leftNode);
list.remove(rightNode);
list.add(parentNode);
}
return list.get(0);
}
}
class HuffmanTreeNode implements Comparable<HuffmanTreeNode>{
int value;
HuffmanTreeNode left;
HuffmanTreeNode right;
public HuffmanTreeNode(int value){
this.value = value;
}
public void preOrder(){
System.out.println(this);
if (this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
@Override
public int compareTo(HuffmanTreeNode o) {
return this.value - o.value;
}
@Override
public String toString() {
return "HuffmanTreeNode{" +
"value=" + value +
'}';
}
}
二叉排序树
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。如果有相同的值,可以将该节点放在左子节点或右子节点,二叉排序树的中序遍历为有序数列。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Yzcy2GF-1664240469550)(/iblog/posts/annex/images/essays/数据结构与算法-030.png)]](https://img-blog.csdnimg.cn/1c47a39e3da04026a2ae148dfc7d80dd.png)
class BinarySortTree {
private Node root;
public void setRoot(Node root) {
this.root = root;
}
public void add(Node node) {
if (this.root == null) {
this.root = node;
} else {
this.root.add(node);
}
}
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
}
}
public Node search(int target) {
if (this.root == null) {
return null;
}
return this.root.search(target);
}
public Node searchParentNode(int target) {
if (this.root == null) {
return null;
}
return this.root.searchParentNode(target);
}
public void delNode(int target) {
if (this.root == null) {
return;
}
if (this.root.left == null && this.root.right == null) {
this.root = null;
return;
}
Node targetNode = search(target);
if (targetNode == null) {
return;
}
Node parentNode = searchParentNode(target);
if (targetNode.left == null && targetNode.right == null) {
if (parentNode.left != null && parentNode.left.val == target) {
parentNode.left = null;
} else {
parentNode.right = null;
}
return;
}
if (targetNode.left != null && targetNode.right != null) {
targetNode.val = delRightTreeMin(targetNode.right);
return;
}
if (targetNode.left != null) {
if(parentNode == null){
root = targetNode.left;
return;
}
if (parentNode.left.val == target) {
parentNode.left = targetNode.left;
} else {
parentNode.right = targetNode.left;
}
}
if (targetNode.right != null) {
if(parentNode == null){
root = targetNode.right;
return;
}
if (parentNode.right.val == target) {
parentNode.left = targetNode.right;
} else {
parentNode.right = targetNode.right;
}
}
}
private int delRightTreeMin(Node node) {
Node target = node;
while (target.left != null) {
target = target.left;
}
delNode(target.val);
return target.val;
}
}
class Node {
int val;
Node left;
Node right;
public Node(int val) {
this.val = val;
}
public Node search(int target) {
if (this.val == target) {
return this;
} else if (this.val > target) {
if (this.left == null) {
return null;
}
return this.left.search(target);
} else {
if (this.right == null) {
return null;
}
return this.right.search(target);
}
}
public Node searchParentNode(int target) {
if ((this.left != null && this.left.val == target) || (this.right != null && this.right.val == target)) {
return this;
} else if (this.left != null && this.val > target) {
return this.left.searchParentNode(target);
} else if (this.right != null && this.val <= target) {
return this.right.searchParentNode(target);
} else {
return null;
}
}
public void add(Node node) {
if (node == null) {
return;
}
if (node.val < this.val) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
}
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
@Override
public String toString() {
return "Node{" +
"val=" + val +
'}';
}
}
平衡二叉树
二叉搜索树一定程度上可以提高搜索效率,但是当序列构造二叉搜索树,可能会将二叉树退化成单链表,从而降低搜索效率。
平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高。
平衡二叉树的特点:
- 平衡二叉树一定是二叉排序;
- 是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wVdAw1zG-1664240469551)(/iblog/posts/annex/images/essays/数据结构与算法-031.gif)]](https://img-blog.csdnimg.cn/8a5bc5a0a11a4f6a9d9f9e5a83f1fccd.gif)
将有序二叉树变为平衡二叉树代码
public class AVLTreeDemo {
public static void main(String[] args) {
AVLTree avlTree = new AVLTree();
int[] arr = {10, 11, 7, 6, 8, 9 };
for (int i = 0; i < arr.length; i++) {
Node node = new Node(arr[i]);
avlTree.add(node);
}
System.out.println("当前树高度:"+ avlTree.height());
System.out.println("当前根结点:" + avlTree.getRoot());
System.out.println("当前左子树高度:"+ avlTree.getRoot().leftHeight());
System.out.println("当前右子树高度:"+ avlTree.getRoot().rightHeight());
}
}
class AVLTree{
private Node root;
public void setRoot(Node root) {
this.root = root;
}
public Node getRoot() {
return root;
}
public int height(){
if (root == null){
return 0;
}
return this.root.height();
}
public void add(Node node) {
if (this.root == null) {
this.root = node;
} else {
this.root.add(node);
}
}
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
}
}
}
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
public int leftHeight(){
if (this.left == null){
return 0;
}
return this.left.height();
}
public int rightHeight(){
if (this.right == null){
return 0;
}
return this.right.height();
}
public int height() {
return Math.max(
(this.left == null ? 0 : this.left.height()),
(this.right == null ? 0 : this.right.height())
) + 1;
}
public void leftRote(){
Node newNode = new Node(value);
newNode.left = left;
newNode.right = right.left;
value = right.value;
right = right.right;
left = newNode;
}
public void rightRote(){
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
public void add(Node node) {
if (node == null) {
return;
}
if (node.value < this.value) {
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
if (this.leftHeight() - this.rightHeight() > 1){
if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){
this.left.leftRote();
this.rightRote();
}else {
this.rightRote();
}
return;
}
if (this.rightHeight() - this.leftHeight() > 1) {
if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){
this.right.rightRote();
this.leftRote();
}else {
this.leftRote();
}
}
}
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
多路查找树
在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)。多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。典型的多叉树有:2-3树、2-3-4树、红黑树和B树。
多叉树的前提是有序二叉树。
2-3树
2-3树是由二节点和三节点构成的树,是最简单的B树结构,2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)。
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点;
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KcZ4S1Ix-1664240469551)(/iblog/posts/annex/images/essays/数据结构与算法-032.png)]](https://img-blog.csdnimg.cn/ec0e58785dba43f5999056b0ef603d4f.png)
2-3-4树,与2-3树类似。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BsWjq6Y3-1664240469551)(/iblog/posts/annex/images/essays/数据结构与算法-033.png)]](https://img-blog.csdnimg.cn/0b93ce9359094c7bbdb9ad765106e6d4.png)
B树
B-tree 树即 B 树,B 即 Balanced ,平衡的意思。 B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。
文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4k),这样每个节点只需要一次I/O就可以完全载入。
B树的阶(度):节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。
B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
B树的关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据,搜索有可能在非叶子结点结束,其搜索性能等价于在关键字全集内做一次二分查找。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0oLOCUd-1664240469552)(/iblog/posts/annex/images/essays/数据结构与算法-034.png)]](https://img-blog.csdnimg.cn/cf108176b6a14d47af705e36498a1b06.png)
B+树
B+树是B树的变体,也是一种多路搜索树。
B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
B+树所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。所以不可能在非叶子结点命中。
B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录, B+树更适合文件索引系统,B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rTZcptlx-1664240469552)(/iblog/posts/annex/images/essays/数据结构与算法-035.png)]](https://img-blog.csdnimg.cn/3ef033a7c02d42919f8c0c7f071d3d04.png)
B*树
B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ob6EQq8S-1664240469552)(/iblog/posts/annex/images/essays/数据结构与算法-036.png)]](https://img-blog.csdnimg.cn/25b01b1aa0ed45a6bd8b2d7a401c4262.png)
图
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。
如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U50WwAmU-1664240469553)(/iblog/posts/annex/images/essays/数据结构与算法-037.png)]](https://img-blog.csdnimg.cn/c998d3daf56d41fcbf20d4aa7058f785.png)
可用二维数组表示图(邻接矩阵);或链表表示(邻接表)。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HJ60xnTV-1664240469553)(/iblog/posts/annex/images/essays/数据结构与算法-038.png)]](https://img-blog.csdnimg.cn/ee46509583cc4bf1a19723c236259eeb.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEzFQ47K-1664240469554)(/iblog/posts/annex/images/essays/数据结构与算法-039.png)]](https://img-blog.csdnimg.cn/36021450a55741358a16413281ccd930.png)
用java模拟图,包括图的深度遍历,广度遍历。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5PBCta6J-1664240469555)(/iblog/posts/annex/images/essays/数据结构与算法-040.gif)]](https://img-blog.csdnimg.cn/bf37c313ba074447be7c5bd48adca851.gif)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oJSk2V39-1664240469555)(/iblog/posts/annex/images/essays/数据结构与算法-041.gif)]](https://img-blog.csdnimg.cn/41e643939d0a429d9c176dcf9d8be544.gif)
public class GraphDemo {
public static void main(String[] args) {
String[] vertexes = {"A", "B", "C", "D", "E"};
int n = vertexes.length;
Graph graph = new Graph(n);
for (String vertex : vertexes) {
graph.addVertex(vertex);
}
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 1);
graph.addEdge(1, 3, 1);
graph.addEdge(1, 4, 1);
graph.showEdges();
graph.dfs();
System.out.println();
graph.bfs();
}
}
class Graph {
private List<String> vertexList;
private int sideNums;
private int[][] edges;
private boolean[] isVisited;
public Graph(int n) {
vertexList = new ArrayList<>(n);
edges = new int[n][n];
sideNums = 0;
}
private int getFirstNeighbor(int index) {
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 1) {
return i;
}
}
return -1;
}
private int getNextNeighbor(int vertex, int index) {
for (int i = vertex + 1; i < vertexList.size(); i++) {
if (edges[index][i] > 1) {
return i;
}
}
return -1;
}
private void dfs(boolean[] isVisited, int i) {
System.out.print(getVertexValByIndex(i) + "->");
isVisited[i] = true;
int firstNeighborIndex = getFirstNeighbor(i);
while (firstNeighborIndex != -1) {
if (!isVisited[firstNeighborIndex]) {
dfs(isVisited, firstNeighborIndex);
}
firstNeighborIndex = getNextNeighbor(firstNeighborIndex, i);
}
}
public void dfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < getVertexCount(); i++) {
if (!isVisited[i]) {
dfs(isVisited, i);
}
}
}
private void bfs(boolean[] isVisited, int i) {
int headIndex;
int neighborIndex;
LinkedList<Integer> queue = new LinkedList<>();
System.out.print(getVertexValByIndex(i) + "->");
isVisited[i] = true;
queue.addLast(i);
while (!queue.isEmpty()) {
headIndex = queue.removeFirst();
neighborIndex = getFirstNeighbor(headIndex);
while (neighborIndex != -1) {
if (!isVisited[neighborIndex]) {
System.out.print(getVertexValByIndex(neighborIndex) + "->");
isVisited[neighborIndex] = true;
queue.addLast(neighborIndex);
}
neighborIndex = getNextNeighbor(headIndex, neighborIndex);
}
}
}
public void bfs() {
isVisited = new boolean[vertexList.size()];
for (int i = 0; i < getVertexCount(); i++) {
if (!isVisited[i]) {
bfs(isVisited, i);
}
}
}
public void addVertex(String vertex) {
vertexList.add(vertex);
}
public void addEdge(int vertex1, int vertex2, int weight) {
edges[vertex1][vertex2] = weight;
edges[vertex2][vertex1] = weight;
sideNums++;
}
public int getSideNums() {
return sideNums;
}
public void showEdges() {
for (int[] edge : edges) {
System.out.println(Arrays.toString(edge));
}
}
public int getVertexCount() {
return vertexList.size();
}
public int getVertexWeight(int vertex1, int vertex2) {
return edges[vertex1][vertex2];
}
public String getVertexValByIndex(int index) {
return vertexList.get(index);
}
}
算法
英文对应的单词是Algorithm,它的本意为:解决问题的方法,所以算法的直接理解就是解决问题的方法。在计算机领域定义的话就是:一系列解决问题的、清晰、可执行的计算机指令。
一个算法的优劣可以用空间复杂度与时间复杂度来衡量。
算法复杂度
时间复杂度
度量一个算法执行时间的两种方法:
-
事后统计法:即直接运行程序,统计需要的时间和空间。但是,这种方法有两个问题:
- 结果非常依赖于测试环境。比如,用i3和用i8运行程序所需的时间是不同的;
- 结果受测试规模的影响特别大。比如,对有序数组进行排序的时间比对逆序数组排序的时间短;对于小规模数据而言,插入排序所需时间比快速排序要短;
所以,就需要有一种不用具体测试数据,也能估计算法执行效率的方法,就是算法复杂度分析,包括时间、空间复杂度分析。 -
事前估算法:通过分析某个算法的时间复杂度来判断那个算法更优;
一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数, 用T(n) 表示,若有某个辅助函数f(n) ,使得当 n 趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常数,则称f(n) 是T(n) 的同数量级函数。记作 T(n)=O(f(n)) , 称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
例如,T(n) = n + 1 与 T(n) = n 就是同数量级函数,因为 n+1/n 的极限值为不等于零的常数。 T(n) 不同,但时间复杂度可能相同: T(n)=n2+7n+6 与 T(n)=3n2+2n+2 它们的 T(n) 不同, 但时间复杂度相同,都为 O(n2)。
计算时间复杂度的方法:
- 用常数 1 代替运行时间中的所有加法常数: T(n)=n2+7n+6 => T(n)=n2+7n+1
- 修改后的运行次数函数中,只保留最高阶项: T(n)=n2+7n+1 => T(n) = n2
- 去除最高阶项的系数:T(n) = n2 => T(n) = n2 => O(n2)
时间频度
一个算法花费的时间与算法中的语句的执行次数成正比例,哪个算法执行次数多,他花费的时间就多.一个算法中的语句执行次数称为语句频度或时间频度.记为T(n)。
随着时间的推移,一些复杂度花费时间无限接近:
- 忽略常数项:
- 2n+20 和 2n 随着 n 变大,执行曲线无限接近,20 可以忽略
- 3n+10 和 3n 随着 n 变大,执行曲线无限接近,10 可以忽略
- 忽略低次项:
- 2n2+3n+10 和 2n2 随着 n 变大, 执行曲线无限接近,可以忽略 3n+10
- n2+5n+20 和 n2 随着 n 变大,执行曲线无限接近,可以忽略 5n+20
- 忽略系数:
- 随着 n 值变大, 5n2+7n 和 3n2 + 2n ,执行曲线重合, 说明这种情况下, 5 和 3 可以忽略
- 对于 n^3+5n 和 6n^3+4n,执行曲线分离,不能忽略系数,说明多少次方是关键
常见的时间复杂度
-
常数阶 O(1): 无论代码执行了多少行,只要是没有循环等复杂结构,这个代码的时间复杂度就都是O(1); int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
-
对数阶 O(log2n) ![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QulUd91d-1664240469556)(/iblog/posts/annex/images/essays/数据结构与算法-012.png)]](https://img-blog.csdnimg.cn/9c59f1fde1274c579002d4409581d89b.png) int i = 1;
while(i<n){
i = i*2;
}
-
线性阶 O(n): 它消耗的时间是随着n的变化而变化的,与n成正比或反比; for(int i=1; i<=n; ++i){
j = i;
j++;
}
-
线性对数阶 O(nlog2n): 将时间复杂度为O(logn)的代码循环n遍,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN); for(int m=1; m<=n; ++m){
int i = 1;
while(i<n){
i = i*2;
}
}
-
平方阶 O(n^2): 如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n2),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n) ,即 O(n2) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n) ; for(int i=1; i<=n; ++i){
for(int j=1; j<=n; ++j){
j=i;
}
}
-
立方阶 O(n^3) for(int i=1; i<=n; ++i){
for(int j=1; j<=n; ++j){
for(int x=1; x<=n; ++x){
int m = 0;
i = x+j;
}
}
}
-
k 次方阶 O(n^k) -
指数阶 O(2^n)
常见的算法时间复杂度由小到大依次为:
Ο (1)<Ο (log2n)<Ο (n)<Ο (nlog2n)<Ο (n2)<Ο (n3)< Ο (nk) < Ο (2n)
随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
空间复杂度
类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。
空间复杂度全称为渐进空间复杂度,是对一个算法在运行过程中临时占用存储空间大小的量度。 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关, 它随着 n 的增大而增大, 当 n 较大时, 将占用较多的存储单元, 例如快速排序和归并排序算法, 基数排序就属于这种情况 在做算法分析时, 主要讨论的是时间复杂度。 从用户使用体验上看, 更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间
空间复杂度较为简单,常见的空间复杂度为 O(1),O(n) 和 O(n ^ 2)。
递归
递归就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
递归能解决什么问题?
- 各种数学问题如: 8 皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题(google 编程大赛);
- 各种算法中也会使用到递归, 比如快排, 归并排序, 二分查找, 分治算法等;
- 用栈解决的问题,使用递归替换代码更佳简洁;
使用递归遵守的规则:
- 执行一个方法时,就创建一个新的受保护的独立空间(一个线程有自己独立的一个栈空间,每个方法调用对应着一个栈帧);
- 方法的局部变量是独立的,不会相互影响;
- 如果方法中使用的是引用类型变量,就会共享该引用类型的数据,比如数组;
- 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError;
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕;
迷宫回溯
迷宫回溯问题,寻找最短路径可以通过改变策略,将每个策略都经过的路保存在集合中,最后看哪个集合最小即最小路径。
public class MyTest {
public static void main(String[] args) {
Mazeback mazeback = new Mazeback();
int[][] map = mazeback.createMap();
mazeback.list(map);
System.out.println("自动寻找路线:");
mazeback.setWay(map, 1, 1);
mazeback.list(map);
}
}
class Mazeback {
public int[][] createMap() {
int[][] map = new int[7][8];
for (int i = 0; i < 7; i++) {
map[i][0] = 1;
map[i][7] = 1;
map[6][i] = 1;
map[0][i] = 1;
}
map[3][1] = 1;
map[3][2] = 1;
return map;
}
public void list(int[][] map) {
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + "");
}
System.out.println();
}
}
public boolean setWay(int[][] map, int row, int column) {
if (map[5][6] == 2) {
return true;
}
if (map[row][column] == 0) {
map[row][column] = 2;
if (setWay(map, row + 1, column)) {
return true;
} else if (setWay(map, row, column + 1)) {
return true;
} else if (setWay(map, row - 1, column)) {
return true;
} else if (setWay(map, row, column - 1)) {
return true;
} else {
map[row][column] = 3;
return false;
}
}
return false;
}
}
八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8 × 8 格的国际象棋上摆放八个皇后,使其不能互相攻击, 即: 任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
八皇后问题共92中解法
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kSHyCKis-1664240469557)(/iblog/posts/annex/images/essays/数据结构与算法-010.png)]](https://img-blog.csdnimg.cn/142e12029f33409faf619fbfbfa41a61.png)
public class MyTest {
public static void main(String[] args) {
EightQueens eightQueens = new EightQueens();
eightQueens.exec(0);
}
}
class EightQueens {
private int[] arr;
private int max;
public EightQueens() {
this.max = 8;
this.arr = new int[max];
}
public void exec(int position) {
if (position == max) {
print();
return;
}
for (int i = 0; i < max; i++) {
arr[position] = i;
if (check(position)){
exec(position + 1);
}
}
}
private boolean check(int position) {
for (int j = 0; j < position; j++) {
if (arr[position] == arr[j] || Math.abs(position - j) == Math.abs(arr[position] - arr[j])) {
return false;
}
}
return true;
}
private void print() {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "");
}
System.out.println();
}
}
排序算法
排序也称排序算法,是将一组数据,依指定的顺序进行排列的过程。
排序算法分类:
- 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序;
- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AyWQOoKA-1664240469558)(/iblog/posts/annex/images/essays/数据结构与算法-011.png)]](https://img-blog.csdnimg.cn/d7dae0945f1f4732913de3fce68bbd7f.png)
常见内排序算法复杂度比较
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7DujgJHQ-1664240469559)(/iblog/posts/annex/images/essays/数据结构与算法-013.png)]](https://img-blog.csdnimg.cn/b79b7589eb7e43228876f95fb54eef57.png)
名词解释:
- n:数据规模
- k:"桶"的个数
- In-place:占用常数内存,不占用额外内存
- Out-place:占用额外内存
- 稳定性:排序后两个相等键值的顺序和排序之前它们的顺序相同,不稳定性则相反
- 时间复杂度:一个算法执行所耗费的时间
- 空间复杂度:运行完一个程序所需内存的大小
冒泡排序
冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序(当前值大于比较的值)则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMaQVEgC-1664240469559)(/iblog/posts/annex/images/essays/数据结构与算法-014.gif)]](https://img-blog.csdnimg.cn/18a2d0fb45814bf58334d811296a6a4b.gif)
优化:因为排序的过程中, 各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。 从而减少不必要的比较。
class BubbleSorting {
public int[] sort(int[] data) {
int len = data.length - 1;
int tmp;
boolean flag = false;
for (int i = 0; i <len; i++){
for (int j = 0; j < len - i; j++) {
if (data[j] > data[j+1]){
flag = true;
tmp = data[j];
data[j] = data[j+1];
data[j+1] = tmp;
}
}
if (!flag){
break;
}else {
flag = false;
}
}
return data;
}
}
选择排序
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。
首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1azFHXz-1664240469560)(/iblog/posts/annex/images/essays/数据结构与算法-015.gif)]](https://img-blog.csdnimg.cn/5436ae773bfc47aa971d390e8c390b12.gif)
class SelectSorting{
public int[] sort(int[] data) {
for (int i = 0; i < data.length; i++){
int min = i;
for (int j = i+1; j < data.length; j++) {
if (data[i] > data[j]){
min = j;
}
}
if (min != i){
int tmp = data[i];
data[i] = data[min];
data[min] = tmp;
}
}
return data;
}
}
插入排序
插入排序(Insertion Sorting) 的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表;
开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较, 将它插入到有序表中的适当位置,使之成为新的有序表。
对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RPZXTF39-1664240469560)(/iblog/posts/annex/images/essays/数据结构与算法-016.gif)]](https://img-blog.csdnimg.cn/460a71d79cd746bfac97622fb9240498.gif)
class InsertSorting{
public int[] sort(int[] data) {
for (int i = 1; i < data.length; i++){
if (data[i] > data[i - 1]){
continue;
}
int tmp = data[i];
int index = i - 1;
while (index >= 0 && tmp < data[index]){
data[index + 1] = data[index];
index--;
}
data[index + 1] = tmp;
}
return data;
}
}
希尔排序
希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYFEryNY-1664240469561)(/iblog/posts/annex/images/essays/数据结构与算法-017.gif)]](https://img-blog.csdnimg.cn/2ec19eccd2324e2eac5336a483d0e55b.gif)
class ShellSorting{
public void sortSwap(int[] data){
int size = data.length;
int tmp = 0;
for (int gap = size >> 1; gap > 0; gap >>= 1){
for (int i = gap; i < size; i++) {
for (int j = i - gap; j >= 0; j -= gap) {
if (data[j] > data[j + gap]){
tmp = data[j];
data[j] = data[j+gap];
data[j+gap] = tmp;
}
}
}
}
}
public void sort(int[] data){
int size = data.length;
int tmp = 0;
for (int gap = size >> 1; gap > 0; gap >>= 1) {
for (int i = gap; i < size; i++) {
tmp = data[i];
int index = i;
while (index - gap >= 0 && tmp < data[index - gap]){
data[index] = data[index - gap];
index -= gap;
}
data[index] = tmp;
}
}
}
}
快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序思路:
- 选定pivot基准数
- 将大于pivot基准数放在基准数右边
- 将小于pivot基准数放在基准数左边
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1L2zbxts-1664240469561)(/iblog/posts/annex/images/essays/数据结构与算法-018.gif)]](https://img-blog.csdnimg.cn/c65cd48d09944be7beb2f8171bfb1c56.gif)
class QuickSorting {
public void sort(int[] data, int l, int r) {
if (l >= r){
return;
}
int left = l;
int right = r;
int pivot = data[left];
while (left < right) {
while (left < right && data[right] >= pivot) {
--right;
}
data[left] = data[right];
while (left < right && data[left] <= pivot) {
++left;
}
data[right] = data[left];
}
data[left] = pivot;
sort(data, l, left);
sort(data, right+1,r);
}
}
归并排序
归并排序是利用归并的思想实现的排序方法, 该算法采用经典的分治(divide-and-conquer)策略;分治法将问题分成一些小的问题然后递归求解, 而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
归并排序算法思路:采用分治算法思想,首先将序列使用递归进行拆分,然后进行合并;合并思路,将两个有序队列中的元素分别按顺序进行比较,将结果保存在一个临时数组中,最后将临时数组合并到最后的队列中。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vy9xTpG-1664240469562)(/iblog/posts/annex/images/essays/数据结构与算法-019.gif)]](https://img-blog.csdnimg.cn/3d712e8dd0f641b9bd2da79e4c4d089b.gif)
class MergeSorting {
public void divide(int[] arr, int start, int end, int[] tmpArr) {
if (start >= end) {
return;
}
int mid = (start + end) >> 1;
divide(arr, start, mid, tmpArr);
divide(arr, mid + 1, end, tmpArr);
merge(arr, start, mid, end, tmpArr);
}
public void merge(int[] arr, int start, int mid, int end, int[] tmpArr) {
int leftIndex = start;
int rightIndex = mid + 1;
int tmpArrIndex = 0;
while (leftIndex <= mid && rightIndex <= end) {
if (arr[leftIndex] <= arr[rightIndex]) {
tmpArr[tmpArrIndex] = arr[leftIndex];
++leftIndex;
} else {
tmpArr[tmpArrIndex] = arr[rightIndex];
++rightIndex;
}
++tmpArrIndex;
}
while (leftIndex <= mid) {
tmpArr[tmpArrIndex] = arr[leftIndex];
++leftIndex;
++tmpArrIndex;
}
while (rightIndex <= end) {
tmpArr[tmpArrIndex] = arr[rightIndex];
++rightIndex;
++tmpArrIndex;
}
tmpArrIndex = 0;
int tmpLeftIndex = leftIndex;
while (tmpLeftIndex <= end) {
arr[tmpLeftIndex] = tmpArr[tmpArrIndex];
++tmpLeftIndex;
++tmpArrIndex;
}
}
}
基数排序
基数排序是 1887 年赫尔曼·何乐礼发明的。基数排序属于“分配式排序”,又称“桶子法”或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用,基数排序法是属于稳定性的排序, 基数排序法的是效率高的稳定性排序法。
基数排序是桶排序的扩展,它是这样实现的: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零;然后, 从最低位开始, 依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n2eH9oXR-1664240469564)(/iblog/posts/annex/images/essays/数据结构与算法-020.gif)]](https://img-blog.csdnimg.cn/d998c463418f44b4a73b9253759da655.gif)
class BucketSorting {
public void sort(int[] arr) {
int maxNum = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > maxNum) {
maxNum = arr[i];
}
}
int maxNumLen = String.valueOf(maxNum).length();
int[][] buckets = new int[10][arr.length];
int[] bucketElementIndex = new int[10];
for (int i = 0, n = 1; i < maxNumLen; i++, n *= 10) {
for (int j = 0; j < arr.length; j++) {
int index = arr[j] / n % 10;
buckets[index][bucketElementIndex[index]] = arr[j];
bucketElementIndex[index]++;
}
int index = 0;
for (int f = 0; f < bucketElementIndex.length; f++) {
if (bucketElementIndex[f] == 0) {
continue;
}
for (int h = 0; h < bucketElementIndex[f]; h++) {
arr[index] = buckets[f][h];
index++;
}
bucketElementIndex[f] = 0;
}
}
}
}
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vLpQOjm3-1664240469565)(/iblog/posts/annex/images/essays/数据结构与算法-027.png)]](https://img-blog.csdnimg.cn/3876a21c4de74878a8f6cdede2392984.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZsmZckUN-1664240469566)(/iblog/posts/annex/images/essays/数据结构与算法-026.gif)]](https://img-blog.csdnimg.cn/77cbaa51d98a4849a344e52b72409aee.gif)
class HeapSorting {
public void sort(int[] arr) {
int leafNode = (arr.length >> 1) - 1;
for (int i = leafNode; i >= 0; i--) {
buildMaxHeap(arr, i, arr.length);
}
int tmp;
for (int i = arr.length - 1; i > 0; i--) {
tmp = arr[i];
arr[i] = arr[0];
arr[0] = tmp;
buildMaxHeap(arr, 0, i);
}
}
public void buildMaxHeap(int[] arr, int i, int len) {
int tmp = arr[i];
for (int n = (i << 1) + 1; n < len; n = (n << 1) + 1) {
if (n + 1 < len && arr[n] < arr[n + 1]) {
n++;
}
if (arr[n] > tmp) {
arr[i] = arr[n];
i = n;
} else {
break;
}
}
arr[i] = tmp;
}
}
查找算法
线性查找
线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的K值相等,则查找成功;若比较结果与文件中n个记录的关键字都不等,则查找失败。
class LinearSearch{
public int search(int[] arr, int value){
for (int i = 0; i < arr.length; i++){
if (arr[i] == value) {
return i;
}
}
return - 1;
}
}
二分查找
二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
二分查找算法的前提,数组必须是有序数组,如果没有有序列表,请使用排序算法对列表进行排序。
递归实现二分查找
class BinarySearch {
public int search(int start, int end, int[] arr, int value) {
if (start > end || value > arr[end] || value < arr[start]) {
return -1;
}
int mid = (start + end) >> 1;
int midVal = arr[mid];
if (value < midVal) {
return search(start, mid - 1, arr, value);
}
if (value > midVal) {
return search(mid + 1, end, arr, value);
}
return mid;
}
}
非递归实现二分查找
public class BinarySearchDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(new BinarySearch().search(arr, 3));
}
}
class BinarySearch {
public int search(int[] arr, int target) {
int left = 0;
int right = arr.length - 1;
while (left <= right) {
int mid = (left + right) >> 1;
if (arr[mid] == target) {
return mid;
}
if (target < arr[mid]) {
right = mid - 1;
}
if (target > arr[mid]) {
left = mid + 1;
}
}
return -1;
}
}
插值查找
插值查找算法类似于二分查找,与二分查找不同的是插值查找每次从自适应 mid 处开始查找,而不是像二分查找那样每次都从中间开始找。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JrolLc7c-1664240469567)(/iblog/posts/annex/images/essays/数据结构与算法-021.png)]](https://img-blog.csdnimg.cn/9b49698585d94ab2aa93bd18bfbccc84.png)
注意:对于数据量较大,关键字分布比较均匀(最好是线性分布)的查找表来说,采用插值查找,速度较快;对于关键字分布不均匀的情况下,该方法不一定比二分查找要好。
class InsertValueSearch {
public int search(int start, int end, int[] arr, int value) {
if (start > end || value > arr[end] || value < arr[start]) {
return -1;
}
int mid = start + (value - arr[start]) / (arr[end] - arr[start]) * (end - start);
int midVal = arr[mid];
if (value < midVal) {
return search(start, mid - 1, arr, value);
}
if (value > midVal) {
return search(mid + 1, end, arr, value);
}
return mid;
}
}
斐波那契查找
斐波那契查找是基于【黄金分割】的二分查找。即在斐波那契队列中,将二分查找中的分割点替换为黄金分割点,来查找。
黄金分割是指将整体一分为二,较大部分与整体部分的比值等于较小部分与较大部分的比值,其 比值 约为 0.618。这个比例被公认为是最能引起美感的 比例,因此被称为黄金分割。
斐波那契查找特点:
- 平均性能「斐波那契查找」好于「二分查找」;
- 「斐波那契查找」计算 mid 的时候 使用加减法而不是除法,会微弱提升效率;
class FibonacciSearch{
public static int search(int[] lookupTable,int[] f,int target){
int low = 0;
int high = lookupTable.length - 1;
int k = 0;
int middle = 0;
while (f[k] < high){
k ++;
}
int[] temp = Arrays.copyOf(lookupTable,f[k]);
while (low <= high){
middle = low + f[k - 1];
if (target < lookupTable[middle]){
high = middle -1;
k --;
}else if (target > lookupTable[middle]){
low = middle + 1;
k -= 2;
}else{
return Math.min(middle,high);
}
}
return -1;
}
}
哈夫曼编码
赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
哈夫曼编码是哈夫曼树在电讯通信中的经典的应用之一。哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。哈夫曼码是可变字长编码的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。
定长编码与变长编码,以字符串like like为例:
- 将上述字符串转换对应的ASCII: 108 105 107 101 32 108 105 107 101
- ASCII转换为二进制:01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101
- 统计上述字符串出现的各字符出现的次数:l:2 i:2 k:2 e:2 :1
- 按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小:0=l 1=i 10=k 11=e 100=
- 最终转换为变长编码为:011011100011011
上述的变长编码 011011100011011 在解码的时候会出现多意现象,比如当匹配到数字1,是把1解成i还是按照10来进行解码。因为这种现象的存在,所以在进行变长编码时,编码要符合前缀编码。
字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。
构建哈夫曼编码思路:
- 统计字节数组中各个数据的权重,即字符出现的次数;
- 将统计好的字符出现的次数,构建成哈夫曼树;
- 根据上面创建的哈夫曼树获得每个数值对应的可变长编码值,规定结点的左边为0 ,右边为1;
- 以每个数值新的编码重新对字符数组进行编码,即可得到赫夫曼编码后的值;
假如一段信息里只有A,B,C,D,E,F这6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,最终构建成哈夫曼编树为下图所示:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eLdHS6GU-1664240469567)(/iblog/posts/annex/images/essays/数据结构与算法-029.png)]](https://img-blog.csdnimg.cn/08b25056547c4413be27d7ef237c0429.png)
得到哈夫曼编码:
A=11100 B=11101 C=1111 D=110 E=10 F=0
利用哈夫曼编码,压缩解压文件:
public class HuffmanCodeTest {
public static void main(String[] args) {
String srcFile = "/Users/whitepure/Desktop/1.png";
String dstFile = "/Users/whitepure/Desktop/1.zip";
zipFile(srcFile, dstFile);
System.out.println("压缩文件成功");
srcFile = "/Users/whitepure/Desktop/1.zip";
dstFile = "/Users/whitepure/Desktop/1copy.png";
unZipFile(srcFile, dstFile);
System.out.println("解压成功!");
}
public static void zipFile(String srcFile, String dstFile) {
OutputStream os = null;
ObjectOutputStream oos = null;
FileInputStream is = null;
try {
is = new FileInputStream(srcFile);
byte[] b = new byte[is.available()];
is.read(b);
HuffmanCode huffmanCode = new HuffmanCode();
byte[] huffmanBytes = huffmanCode.encode(b);
os = new FileOutputStream(dstFile);
oos = new ObjectOutputStream(os);
oos.writeObject(huffmanBytes);
oos.writeObject(huffmanCode.getHuffmanCodes());
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
is.close();
oos.close();
os.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
public static void unZipFile(String zipFile, String dstFile) {
InputStream is = null;
ObjectInputStream ois = null;
OutputStream os = null;
try {
is = new FileInputStream(zipFile);
ois = new ObjectInputStream(is);
byte[] huffmanBytes = (byte[]) ois.readObject();
Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
HuffmanCode huffmanCode = new HuffmanCode();
byte[] bytes = huffmanCode.decode(huffmanCodes, huffmanBytes);
os = new FileOutputStream(dstFile);
os.write(bytes);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
try {
os.close();
ois.close();
is.close();
} catch (Exception e2) {
System.out.println(e2.getMessage());
}
}
}
}
class HuffmanCode {
private final Map<Byte, String> huffmanCodes = new HashMap<>();
public Map<Byte, String> getHuffmanCodes() {
return huffmanCodes;
}
public byte[] encode(byte[] bytes) {
List<Node> nodes = buildHuffmanNodes(bytes);
Node huffmanTreeRoot = buildHuffmanTree(nodes);
Map<Byte, String> huffmanCodes = buildHuffmanCodeTab(huffmanTreeRoot);
return zip(bytes, huffmanCodes);
}
public byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < huffmanBytes.length - 1; i++) {
byte b = huffmanBytes[i];
String strToAppend = byteToBitString(b);
boolean isLastByte = (i == huffmanBytes.length - 2);
if (isLastByte) {
byte validBits = huffmanBytes[huffmanBytes.length - 1];
strToAppend = strToAppend.substring(0, validBits);
}
stringBuilder.append(strToAppend);
}
Map<String, Byte> map = new HashMap<>();
huffmanCodes.forEach((key, value) -> map.put(value, key));
List<Byte> list = new ArrayList<>();
for (int i = 0; i < stringBuilder.length(); ) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
String key = stringBuilder.substring(i, i + count);
b = map.get(key);
if (b == null) {
count++;
} else {
flag = false;
}
}
list.add(b);
i += count;
}
byte[] b = new byte[list.size()];
IntStream.range(0, b.length).forEach(i -> b[i] = list.get(i));
return b;
}
private List<Node> buildHuffmanNodes(byte[] bytes) {
ArrayList<Node> nodes = new ArrayList<>();
Map<Byte, Integer> counts = new HashMap<>();
for (byte b : bytes) {
counts.merge(b, 1, Integer::sum);
}
counts.forEach((key, value) -> nodes.add(new Node(key, value)));
return nodes;
}
private Node buildHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
Collections.sort(nodes);
Node leftNode = nodes.get(0);
Node rightNode = nodes.get(1);
Node parent = new Node(null, leftNode.weight + rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
return nodes.get(0);
}
private Map<Byte, String> buildHuffmanCodeTab(Node root) {
if (root == null) {
return null;
}
buildHuffmanCodeTab(root.left, "0", new StringBuilder());
buildHuffmanCodeTab(root.right, "1", new StringBuilder());
return huffmanCodes;
}
private void buildHuffmanCodeTab(Node node, String code, StringBuilder stringBuilder) {
StringBuilder curNodeCode = new StringBuilder(stringBuilder);
curNodeCode.append(code);
if (node == null) {
return;
}
if (node.data == null) {
buildHuffmanCodeTab(node.left, "0", curNodeCode);
buildHuffmanCodeTab(node.right, "1", curNodeCode);
} else {
huffmanCodes.put(node.data, curNodeCode.toString());
}
}
private byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
StringBuilder stringBuilder = new StringBuilder();
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
int len;
byte countToEight = (byte) (stringBuilder.length() & 7);
if (countToEight == 0) {
len = stringBuilder.length() >> 3;
} else {
len = (stringBuilder.length() >> 3) + 1;
for (int i = countToEight; i < 8; i++) {
stringBuilder.append("0");
}
}
byte[] huffmanCodeBytes = new byte[len + 1];
huffmanCodeBytes[len] = countToEight;
int index = 0;
for (int i = 0; i < stringBuilder.length(); i += 8) {
String strByte;
strByte = stringBuilder.substring(i, i + 8);
huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
index++;
}
return huffmanCodeBytes;
}
private String byteToBitString(byte b) {
int temp = b;
temp |= 0x100;
String binaryStr = Integer.toBinaryString(temp);
return binaryStr.substring(binaryStr.length() - 8);
}
}
class Node implements Comparable<Node> {
Byte data;
int weight;
Node left;
Node right;
public Node(Byte data, int weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
public String toString() {
return "Node [data = " + data + " weight=" + weight + "]";
}
}
分治算法
分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
使用分治算饭解决汉诺塔问题
汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
public class HanoiTowerDemo {
public static void main(String[] args) {
HanoiTower hanoiTower = new HanoiTower();
hanoiTower.hanoiTower(3, 'A', 'B', 'C');
}
}
class HanoiTower {
public void hanoiTower(int n, char a, char b, char c) {
if (n <= 0) {
return;
}
if (n == 1) {
System.out.println(a + "->" + c);
return;
}
hanoiTower(n - 1, a, c, b);
System.out.println(a + "->" + c);
hanoiTower(n - 1, b, a, c);
}
}
动态规划算法
动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )
关于动态规划最经典的问题当属背包问题。
背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。 | 物品 | 重量 | 价格 | |-----|-----|-----| | 吉他(G) | 1 | 1500 | | 音响(S) | 4 | 3000 | | 电脑(L) | 3 | 2000 |
public class KnapsackProblemDemo {
public static void main(String[] args) {
KnapsackProblem knapsackProblem = new KnapsackProblem();
System.out.println(knapsackProblem.knapsackProblem());
}
}
class KnapsackProblem {
public int knapsackProblem() {
int[] w = {1, 4, 3};
int[] val = {1500, 3000, 2000};
int m = 4;
int n = val.length;
int[][] v = new int[n + 1][m + 1];
for (int i = 0; i < v.length; i++) {
v[i][0] = 0;
}
for (int i = 0; i < v[0].length; i++) {
v[0][i] = 0;
}
for (int i = 1; i < v.length; i++) {
for (int j = 1; j < v[0].length; j++) {
if (w[i - 1] > j) {
v[i][j] = v[i - 1][j];
} else {
v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
}
}
}
int max = 0;
for (int[] ints : v) {
System.out.println(Arrays.toString(ints));
for (int anInt : ints) {
if (anInt > max) {
max = anInt;
}
}
}
return max;
}
}
KMP算法
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
-
常规算法匹配字符串 ![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sq6KguLV-1664240469568)(/iblog/posts/annex/images/essays/数据结构与算法-042.gif)]](https://img-blog.csdnimg.cn/d5685eeb3b2e4fefa33670bd6bb0b689.gif) 从主串的起始位置(或指定位置)开始与模式串的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符再重新和模式串的字符比较。依次类推,直到模式串成功匹配,返回主串中第一次出现模式串字符的位置,或者模式串匹配不成功,这里约定返回-1。 -
KMP算法匹配字符串 ![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y5oZMmKq-1664240469569)(/iblog/posts/annex/images/essays/数据结构与算法-043.gif)]](https://img-blog.csdnimg.cn/75b24d25b59546f29a46a2a11e1fcfe8.gif) 主要就是改进了暴力匹配中i回溯的操作,KMP算法中当一趟匹配过程中出现字符比较不等时,不直接回溯i,而是利用已经得到的“部分匹配”的结果将模式串向右移动(j-next[j-1])的距离。 public class KMPDemo {
public static void main(String[] args) {
KMP kmp = new KMP();
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmp.getMatchTab(str2);
System.out.println(Arrays.toString(next));
System.out.println(kmp.kmpSearch(str1, str2, next));
}
}
class KMP {
public int[] getMatchTab(String dest) {
int[] result = new int[dest.length()];
result[0] = 0;
for (int i = 1, j = 0; i < result.length; i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = result[j - 1];
}
if (dest.charAt(j) == dest.charAt(i)) {
j++;
}
result[i] = j;
}
return result;
}
public int kmpSearch(String str1, String str2, int[] next) {
for (int i = 0, j = 0; i < str1.length(); i++) {
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
}
贪心算法
贪心算法又称贪婪算法,是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
举例,假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。
广播台 | 覆盖地区 |
---|
K1 | “北京”, “上海”, “天津” | K2 | “广州”, “北京”, “深圳” | K3 | “成都”, “上海”, “杭州” | K4 | “上海”, “天津” | K5 | “杭州”, “大连” |
public class GreedyAlgorithmDemo {
public static void main(String[] args) {
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
hashSet5.add("杭州");
hashSet5.add("大连");
broadcasts.put("K1", hashSet1);
broadcasts.put("K2", hashSet2);
broadcasts.put("K3", hashSet3);
broadcasts.put("K4", hashSet4);
broadcasts.put("K5", hashSet5);
HashSet<String> allAreas = new HashSet<>();
for (Map.Entry<String, HashSet<String>> broadcast : broadcasts.entrySet()) {
allAreas.addAll(broadcast.getValue());
}
System.out.println(new GreedyAlgorithm().getRadioByGreedyAlgorithm(allAreas, broadcasts));
}
}
class GreedyAlgorithm {
public List<String> getRadioByGreedyAlgorithm(HashSet<String> allAreas, HashMap<String, HashSet<String>> broadcasts) {
ArrayList<String> selects = new ArrayList<>();
String maxKey = null;
HashSet<String> tmpSet = new HashSet<>();
while (allAreas.size() > 0) {
maxKey = null;
for (String key : broadcasts.keySet()) {
tmpSet.clear();
tmpSet.addAll(broadcasts.get(key));
tmpSet.retainAll(allAreas);
if (tmpSet.size() > 0 && (maxKey == null || tmpSet.size() > broadcasts.get(maxKey).size())) {
maxKey = key;
}
}
if (maxKey != null) {
selects.add(maxKey);
allAreas.removeAll(broadcasts.get(maxKey));
}
}
return selects;
}
}
普里姆算法
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。
最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。简称MST。 求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。
- 普里姆算法:O(n^2),适合稠密图(边多的图)
- 克鲁斯卡尔算法:O,适合稀疏图(边少的图)
普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xxKqGIDh-1664240469569)(/iblog/posts/annex/images/essays/数据结构与算法-044.gif)]](https://img-blog.csdnimg.cn/46800857bad1457495a640eb9cb449f6.gif)
public class PrimAlgorithmDemo {
public static void main(String[] args) {
char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int vertex = data.length;
//邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通
int[][] weight = new int[][]{
{10000, 5, 7, 10000, 10000, 10000, 2},
{5, 10000, 10000, 9, 10000, 10000, 3},
{7, 10000, 10000, 10000, 8, 10000, 10000},
{10000, 9, 10000, 10000, 10000, 4, 10000},
{10000, 10000, 8, 10000, 10000, 5, 4},
{10000, 10000, 10000, 4, 5, 10000, 6},
{2, 3, 10000, 10000, 4, 6, 10000}
};
MGraph graph = new MGraph(vertex);
PrimAlgorithm minTree = new PrimAlgorithm();
graph.create(graph, vertex, data, weight);
graph.show(graph);
minTree.prim(graph, 0);
}
}
class PrimAlgorithm {
/**
* 最小生成树问题 prim算法
*
* @param graph 图对象
* @param vertex 开始的顶点
*/
public void prim(MGraph graph, int vertex) {
int i = 0, j = 0;
int row = -1, column = -1;
// 存放已经访问过的顶点
int[] visited = new int[graph.vertex];
// 用1表示两点之间已经连接, 0表示未连接
visited[vertex] = 1;
int minWeight = 10000;
for (int k = 1; k < graph.vertex; k++){
// 比较两点之间的权值,每次都获取最小的权值
for (i = 0; i < graph.vertex; i++) {
for(j = 0; j < graph.vertex; j++){
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){
minWeight = graph.weight[i][j];
row = i;
column = j;
}
}
}
System.out.println("边<" + graph.data[row] + "," + graph.data[column] + "> 权值:" + minWeight);
// 将顶点标记为已经访问过
visited[column] = 1;
// 每次比较完后需要将minWeight重置
minWeight = 10000;
}
}
}
class MGraph {
int vertex;
char[] data;
int[][] weight;
public MGraph(int vertex) {
this.vertex = vertex;
data = new char[vertex];
weight = new int[vertex][vertex];
}
/**
* 创建图的邻接矩阵
*
* @param graph 图对象
* @param vertex 图对应的顶点个数
* @param data 图的各个顶点的值
* @param weight 图的邻接矩阵
*/
public void create(MGraph graph, int vertex, char[] data, int[][] weight) {
int i, j;
for (i = 0; i < vertex; i++) {
graph.data[i] = data[i];
for (j = 0; j < vertex; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
// 显示图的邻接矩阵
public void show(MGraph graph) {
for (int[] link : graph.weight) {
System.out.println(Arrays.toString(link));
}
}
}
克鲁斯卡尔算法
克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。基本思想是, 将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。 直到具有 n 个顶点的连通网筛选出来 n-1(n为顶点个数) 条边为止。
判断是否构成回路: 当每次需要将一条边添加到最小生成树时,判断该边的两个顶点终点是否相同,相同就会构成回路。
关于终点的说明:就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。
举例
- 首先ABCDEFG这7个顶点,在顶点集合中是按照顺序存放的;
- 第一次选择的是EF,毫无疑问这一条边的终点是F;
- 第二次选择的CD的终点D;
- 第三次选择的DE,终点是F,因为此时D和E相连,D又和F相连,所以D的终点是F。而且,因为C和D是相连的,D和E相连,E和F也是相连的,所以C的终点此时变成了F。也就是说,当选择了EF、CD、DE这三条边后,C、D、E的终点都是F。当然F的终点也是F,因为F还没和后面的哪个顶点连接。
- 本来接下来应该选择CE的,但是由于C和E的终点都是F,所以就会形成回路;
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFTXNUtE-1664240469571)(/iblog/posts/annex/images/essays/数据结构与算法-045.gif)]](https://img-blog.csdnimg.cn/a2ee5d431a4d4f468057756b0cb4acff.gif)
public class KruskalCaseDemo {
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int matrix[][] = {
/*B*
{0, 12, INF, INF, INF, 16, 14},
{12, 0, 10, INF, INF, 7, INF},
{INF, 10, 0, 3, 5, 6, INF},
{INF, INF, 3, 0, 4, INF, INF},
{INF, INF, 5, 4, 0, 2, 8},
{16, 7, 6, INF, 2, 0, 9},
{14, INF, INF, INF, 8, 9, 0}};
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
kruskalCase.print();
kruskalCase.kruskal();
}
}
class KruskalCase {
private static final int INF = Integer.MAX_VALUE;
private int edgeNum;
private char[] vertexs;
private int[][] matrix;
public KruskalCase(char[] vertexs, int[][] matrix) {
int vlen = vertexs.length;
this.vertexs = new char[vlen];
for (int i = 0; i < vertexs.length; i++) {
this.vertexs[i] = vertexs[i];
}
this.matrix = new int[vlen][vlen];
for (int i = 0; i < vlen; i++) {
for (int j = 0; j < vlen; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
for (int i = 0; i < vlen; i++) {
for (int j = i + 1; j < vlen; j++) {
if (this.matrix[i][j] != INF) {
edgeNum++;
}
}
}
}
public void print() {
System.out.println("邻接矩阵为: \n");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%12d", matrix[i][j]);
}
System.out.println();
}
}
private void sortEdges(EdgeData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j + 1].weight) {
EdgeData tmp = edges[j];
edges[j] = edges[j + 1];
edges[j + 1] = tmp;
}
}
}
}
private int getPosition(char ch) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i] == ch) {
return i;
}
}
return -1;
}
private EdgeData[] getEdges() {
int index = 0;
EdgeData[] edges = new EdgeData[edgeNum];
for (int i = 0; i < vertexs.length; i++) {
for (int j = i + 1; j < vertexs.length; j++) {
if (matrix[i][j] != INF) {
edges[index++] = new EdgeData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
private int getEnd(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
public void kruskal() {
int index = 0;
int[] ends = new int[vertexs.length];
EdgeData[] rets = new EdgeData[edgeNum];
EdgeData[] edges = getEdges();
sortEdges(edges);
for (int i = 0; i < edgeNum; i++) {
int point1 = getPosition(edges[i].start);
int point2 = getPosition(edges[i].end);
int endPointOfPoint1 = getEnd(ends, point1);
int endPointOfPoint2 = getEnd(ends, point2);
if (endPointOfPoint1 != endPointOfPoint2) {
ends[endPointOfPoint1] = endPointOfPoint2;
rets[index++] = edges[i];
}
}
System.out.println("最小生成树为");
for (int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}
}
class EdgeData {
char start;
char end;
int weight;
public EdgeData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
@Override
public String toString() {
return "EData [<" + start + ", " + end + ">= " + weight + "]";
}
}
迪杰斯特拉算法
迪杰斯特拉算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。迪杰斯特拉算法是基于贪心思想,从起始位置触发,每次寻找与起点位置距离且未访问过的顶点,以该顶点作为中间结点,更新从起点到其他顶点的距离,直到全部顶点都作为了中间结点,并完成了路径更新,算法结束。 它的主要特点是以起始点为中心向外层层扩展,即图的广度优先搜索思想,直到扩展到终点为止。
视频讲解:bilibili
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nSohndQ-1664240469572)(/iblog/posts/annex/images/essays/数据结构与算法-046.gif)]](https://img-blog.csdnimg.cn/f78c1e8e22834c12815add38b041f3d2.gif)
public class DijkstraAlgorithmDemo {
public static void main(String[] args) {
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[] { N, 5, 7, N, N, N, 2 };
matrix[1] = new int[] { 5, N, N, 9, N, N, 3 };
matrix[2] = new int[] { 7, N, N, N, 8, N, N };
matrix[3] = new int[] { N, 9, N, N, N, 4, N };
matrix[4] = new int[] { N, N, 8, N, N, 5, 4 };
matrix[5] = new int[] { N, N, N, 4, 5, N, 6 };
matrix[6] = new int[] { 2, 3, N, N, 4, 6, N };
Graph graph = new Graph(vertex, matrix);
graph.showGraph();
graph.dsj(6);
}
}
class Graph {
private char[] vertex;
private int[][] matrix;
private VisitedVertex vv;
public Graph(char[] vertex, int[][] matrix) {
this.vertex = vertex;
this.matrix = matrix;
}
public void showDijkstra() {
vv.showArrays();
}
public void showGraph() {
for (int[] link : matrix) {
for (int i : link) {
System.out.printf("%8d", i);
}
System.out.println();
}
}
public void dsj(int index) {
vv = new VisitedVertex(vertex.length, index);
update(index);
vv.showArrays();
for (int j = 1; j < vertex.length; j++) {
index = vv.findNextStartPoint();
update(index);
vv.showArrays();
}
}
private void update(int index) {
int len = 0;
for (int j = 0; j < matrix[index].length; j++) {
len = vv.getDis(index) + matrix[index][j];
if (!vv.isVisited(j) && len < vv.getDis(j)) {
vv.updatePre(j, index);
vv.updateDis(j, len);
}
}
}
}
class VisitedVertex {
public int[] alreadyArr;
public int[] preVisited;
public int[] dis;
public VisitedVertex(int length, int index) {
this.alreadyArr = new int[length];
this.preVisited = new int[length];
this.dis = new int[length];
Arrays.fill(dis, 65535);
this.dis[index] = 0;
this.alreadyArr[index] = 1;
}
public boolean isVisited(int index) {
return alreadyArr[index] == 1;
}
public void updateDis(int index, int len) {
dis[index] = len;
}
public void updatePre(int pre, int index) {
preVisited[pre] = index;
}
public int getDis(int index) {
return dis[index];
}
public int findNextStartPoint() {
int min = 65535, index = 0;
for (int i = 0; i < alreadyArr.length; i++) {
if (alreadyArr[i] == 0 && dis[i] < min) {
min = dis[i];
index = i;
}
}
alreadyArr[index] = 1;
return index;
}
public void showArrays() {
System.out.println("核心数组的值如下:");
for (int i : alreadyArr) {
System.out.print(i + " ");
}
System.out.println();
for (int i : dis) {
System.out.print(i + " ");
}
System.out.println();
for (int i : preVisited) {
System.out.print(i + " ");
}
System.out.println();
char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
int count = 0;
for (int i : dis) {
if (i != 65535) {
System.out.print(vertex[count] + "(" + i + ") ");
} else {
System.out.print("N ");
}
count++;
}
System.out.println();
System.out.println();
}
}
弗洛伊德算法
弗洛伊德算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与迪杰斯特拉算法类似。
迪杰斯特拉算法对比弗洛伊德算法: 迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。
- 弗洛伊德算法计算图中各个顶点之间的最短路径
- 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径
public class FloydAlgorithmDemo {
public static void main(String[] args) {
char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int[][] matrix = new int[vertex.length][vertex.length];
final int N = 65535;
matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
matrix[2] = new int[]{7, N, 0, N, 8, N, N};
matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
FloydGraph graph = new FloydGraph(vertex, vertex.length, matrix);
graph.floyd();
graph.show();
}
}
class FloydGraph {
private char[] vertex;
private int[][] pre;
private int[][] dis;
public FloydGraph(char[] vertex, int length, int[][] dis) {
this.vertex = vertex;
this.dis = dis;
this.pre = new int[length][length];
for (int i = 0; i < length; i++) {
Arrays.fill(pre[i], i);
}
}
public void show() {
for (int k = 0; k < dis.length; k++) {
for (int i = 0; i < dis.length; i++) {
System.out.print(vertex[pre[k][i]] + " ");
}
System.out.println();
for (int i = 0; i < dis.length; i++) {
System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
}
System.out.println();
System.out.println();
}
}
public void floyd(){
int len;
for (int m = 0; m < dis.length; m++) {
for (int a = 0; a < dis.length; a++) {
for (int b = 0; b < dis.length; b++) {
len = dis[a][m] + dis[m][b];
if (len < dis[a][b]){
dis[a][b] = len;
pre[a][b] = pre[m][b];
}
}
}
}
}
}
马踏棋盘算法
马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的8×8棋盘0~7的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。
马踏棋盘问题实际上是图的深度优先搜索(DFS)的应用。
public class HouseChessBoardDemo {
public static void main(String[] args) {
HouseChessBoard houseChessBoard = new HouseChessBoard(7, 7, 2, 4);
houseChessBoard.showChessBoard();
}
}
class HouseChessBoard {
private int x;
private int y;
private boolean visited[];
private boolean finished;
private int[][] chessboard;
public HouseChessBoard(int x, int y, int row, int column) {
this.x = x;
this.y = y;
this.visited = new boolean[x * y];
this.chessboard = new int[x][y];
traversalChess(this.chessboard, row-1, column-1, 1);
}
public void showChessBoard(){
for (int[] ints : this.chessboard) {
for (int anInt : ints) {
System.out.print(anInt + "\t");
}
System.out.println();
}
}
public void traversalChess(int[][] chessboard, int row, int column, int step) {
chessboard[row][column] = step;
visited[row * x + column] = true;
ArrayList<Point> nextPos = getNext(new Point(column, row));
sort(nextPos);
while (nextPos.size() > 0) {
Point current = nextPos.remove(0);
if (!visited[current.y * x + current.x]) {
traversalChess(chessboard, current.y, current.x, step + 1);
}
}
if (step < x * y && !finished) {
chessboard[row][column] = 0;
visited[row * x + column] = false;
} else {
finished = true;
}
}
public void sort(ArrayList<Point> points){
points.sort((o1, o2) -> {
int next1 = getNext(o1).size();
int next2 = getNext(o2).size();
return next1 - next2;
});
}
public ArrayList<Point> getNext(Point current) {
ArrayList<Point> ps = new ArrayList<Point>();
Point p1 = new Point();
if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y - 1) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y - 2) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = current.x + 1) < x && (p1.y = current.y - 2) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = current.x + 2) < x && (p1.y = current.y - 1) >= 0) {
ps.add(new Point(p1));
}
if ((p1.x = current.x + 2) < x && (p1.y = current.y + 1) < y) {
ps.add(new Point(p1));
}
if ((p1.x = current.x + 1) < x && (p1.y = current.y + 2) < y) {
ps.add(new Point(p1));
}
if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y + 2) < y) {
ps.add(new Point(p1));
}
if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y + 1) < y) {
ps.add(new Point(p1));
}
return ps;
}
}
|