《图解数据结构与算法》(Java代码实现、注释解析、算法分析)(2)

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

this.next = newNext;

}

//获取下一个节点

public Node next() {

return this.next;

}

//获取节点中的数据

public int getData() {

return this.data;

}

//判断节点是否为最后一个节点

public boolean isLast() {

return next == null;

}

}

测试类

public class Demo {

public static void main(String[] args) {

//创建节点

Node n1 = new Node(1);

Node n2 = new Node(2);

Node n3 = new Node(3);

//追加节点

n1.append(n2).append(n3);

//取出下一个节点数据

System.out.println(n1.next().next().getData()); //3

//判断节点是否为最后一个节点

System.out.println(n1.isLast()); //false

System.out.println(n1.next().next().isLast()); //true

//显示所有节点信息

n1.show(); //1 2 3

//删除一个节点

// n1.next.removeNode();

// n1.show(); //1 2

//插入一个新节点

n1.next.after(new Node(0));

n1.show(); //1 2 0 3

}

}

5.2 循环链表


循环链表概念

单链表的一个变形是单向循环链表,链表中最后一个节点的 next 域不再为 None,而是指向链表的头节点。

在这里插入图片描述

循环链表操作

实现类

//表示一个节点

public class LoopNode {

//节点内容

int data;

//下一个节点

LoopNode next = this; //与单链表的区别,追加了一个this,当只有一个节点时指向自己

public LoopNode(int data) {

this.data = data;

}

//插入一个节点

public void after(LoopNode node) {

LoopNode afterNode = this.next;

this.next = node;

node.next = afterNode;

}

//删除一个节点

public void removeNext() {

LoopNode newNode = this.next.next;

this.next = newNode;

}

//获取下一个节点

public LoopNode next() {

return this.next;

}

//获取节点中的数据

public int getData() {

return this.data;

}

}

测试类

public class Demo {

public static void main(String[] args) {

//创建节点

LoopNode n1 = new LoopNode(1);

LoopNode n2 = new LoopNode(2);

LoopNode n3 = new LoopNode(3);

LoopNode n4 = new LoopNode(4);

//增加节点

n1.after(n2);

n2.after(n3);

n3.after(n4);

System.out.println(n1.next().getData()); //2

System.out.println(n2.next().getData()); //3

System.out.println(n3.next().getData()); //4

System.out.println(n4.next().getData()); //1

}

}

5.3 双向循环链表


双向循环链表概念

在双向链表中有两个指针域,一个是指向前驱结点的prev,一个是指向后继结点的next指针

在这里插入图片描述

双向循环链表操作

实现类

public class DoubleNode {

//上一个节点

DoubleNode pre = this;

//下一个节点

DoubleNode next = this;

//节点数据

int data;

public DoubleNode(int data) {

this.data = data;

}

//增加节点

public void after(DoubleNode node) {

//原来的下一个节点

DoubleNode nextNext = next;

//新节点作为当前节点的下一个节点

this.next = node;

//当前节点作为新节点的前一个节点

node.pre = this;

//原来的下一个节点作为新节点的下一个节点

node.next = nextNext;

//原来的下一个节点的上一个节点为新节点

nextNext.pre = node;

}

//获取下一个节点

public DoubleNode getNext() {

return this.next;

}

//获取上一个节点

public DoubleNode getPre() {

return this.pre;

}

//获取数据

public int getData() {

return this.data;

}

}

测试类

public class Demo {

public static void main(String[] args) {

//创建节点

DoubleNode n1 = new DoubleNode(1);

DoubleNode n2 = new DoubleNode(2);

DoubleNode n3 = new DoubleNode(3);

//追加节点

n1.after(n2);

n2.after(n3);

//查看上一个,自己,下一个节点内容

System.out.println(n2.getPre().getData()); //1

System.out.println(n2.getData()); //2

System.out.println(n2.getNext().getData()); //3

System.out.println(n1.getPre().getData()); //3

System.out.println(n3.getNext().getData()); //1

}

}

第6章 树结构基础

===========================================================================

6.1 为什么要使用树结构


线性结构中不论是数组还是链表,他们都存在着诟病;比如查找某个数必须从头开始查,消耗较多的时间。使用树结构,在插入查找的性能上相对都会比线性结构要好

6.2 树结构基本概念


示意图

在这里插入图片描述

1、根节点:最顶上的唯一的一个;如:A

2、双亲节点:子节点的父节点就叫做双亲节点;如A是B、C、D的双亲节点,B是E、F的双亲节点

3、子节点:双亲节点所产生的节点就是子节点

4、路径:从根节点到目标节点所走的路程叫做路径;如A要访问F,路径为A-B-F

5、节点的度:有多少个子节点就有多少的度(最下面的度一定为0,所以是叶子节点)

6、节点的权:在节点中所存的数字

7、叶子节点:没有子节点的节点,就是没有下一代的节点;如:E、F、C、G

8、子树:在整棵树中将一部分看成也是一棵树,即使只有一个节点也是一棵树,不过这个树是在整个大树职中的,包含的关系

9、:就是族谱中有多少代的人;如:A是1,B、C、D是2,E、F、G是3

10、树的高度:树的最大的层数:就是层数中的最大值

11、森林:多个树组成的集合

6.3 树的种类


无序树:树中任意节点的子节点之间没有顺序关系,这种树称为无序树,也称为自由树;

有序树:树中任意节点的子节点之间有顺序关系,这种树称为有序树;

  • 二叉树:每个节点最多含有两个子树的树称为二叉树;

  • 完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树;

  • 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树;

  • 排序二叉树(二叉查找树(英语:Binary Search Tree),也称二叉搜索树、有序二叉树);

  • 霍夫曼树(用于信息编码):带权路径最短的二叉树称为哈夫曼树或最优二叉树;

  • B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。

6.4 树的存储与表示


顺序存储:将数据结构存储在固定的数组中,然在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。二叉树通常以链式存储。

在这里插入图片描述

链式存储

在这里插入图片描述

由于对节点的个数无法掌握,常见树的存储表示都转换成二叉树进行处理,子节点个数最多为2

6.5 常见的一些树的应用场景


1、xml,html等,那么编写这些东西的解析器的时候,不可避免用到树

2、路由协议就是使用了树的算法

3、mysql数据库索引

4、文件系统的目录结构

5、所以很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构

第7章 二叉树大全

===========================================================================

7.1 二叉树的定义


任何一个节点的子节点数量不超过 2,那就是二叉树;二叉树的子节点分为左节点和右节点,不能颠倒位置

7.2 二叉树的性质(特性)


性质1:在二叉树的第i层上至多有2^(i-1)个结点(i>0)

性质2:深度为k的二叉树至多有2^k - 1个结点(k>0)

性质3:对于任意一棵二叉树,如果其叶结点数为N0,而度数为2的结点总数为N2,则N0=N2+1;

性质4:具有n个结点的完全二叉树的深度必为 log2(n+1)

性质5:对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为2i,其右孩子编号必为2i+1;其双亲的编号必为i/2(i=1 时为根,除外)

7.3 满二叉树与完全二叉树


满二叉树: 所有叶子结点都集中在二叉树的最下面一层上,而且结点总数为:2^n-1 (n为层数 / 高度)

完全二叉树: 所有的叶子节点都在最后一层或者倒数第二层,且最后一层叶子节点在左边连续,倒数第二层在右边连续(满二叉树也是属于完全二叉树)(从上往下,从左往右能挨着数满)

在这里插入图片描述

7.4 链式存储的二叉树


创建二叉树:首先需要一个树的类,还需要另一个类用来存放节点,设置节点;将节点放入树中,就形成了二叉树;(节点中需要权值,左子树,右子树,并且都能对他们的值进行设置)。

树的遍历

  • 先序遍历:根节点,左节点,右节点(如果节点有子树,先从左往右遍历子树,再遍历兄弟节点)

先序遍历结果为:A B D H I E J C F K G

在这里插入图片描述

  • 中序遍历:左节点,根节点,右节点(中序遍历可以看成,二叉树每个节点,垂直方向投影下来(可以理解为每个节点从最左边开始垂直掉到地上),然后从左往右数)

中遍历结果为:H D I B E J A F K C G

在这里插入图片描述

  • 后序遍历:左节点,右节点,根节点

后序遍历结果:H I D J E B K F G C A

在这里插入图片描述

  • 层次遍历:从上往下,从左往右

层次遍历结果:A B C D E F G H I J K

在这里插入图片描述

查找节点:先对树进行一次遍历,然后找出要找的那个数;因为有三种排序方法,所以查找节点也分为先序查找,中序查找,后序查找;

删除节点:由于链式存储,不能找到要删的数直接删除,需要找到他的父节点,然后将指向该数设置为null;所以需要一个变量来指向父节点,找到数后,再断开连接。

代码实现

在这里插入图片描述

  • 树类

public class BinaryTree {

TreeNode root;

//设置根节点

public void setRoot(TreeNode root) {

this.root = root;

}

//获取根节点

public TreeNode getRoot() {

return root;

}

//先序遍历

public void frontShow() {

if (root != null) {

root.frontShow();

}

}

//中序遍历

public void middleShow() {

if (root != null) {

root.middleShow();

}

}

//后序遍历

public void afterShow() {

if (root != null) {

root.afterShow();

}

}

//先序查找

public TreeNode frontSearch(int i) {

return root.frontSearch(i);

}

//删除一个子树

public void delete(int i) {

if (root.value == i) {

root = null;

} else {

root.delete(i);

}

}

}

  • 节点类

public class TreeNode {

//节点的权

int value;

//左儿子

TreeNode leftNode;

//右儿子

TreeNode rightNode;

public TreeNode(int value) {

this.value = value;

}

//设置左儿子

public void setLeftNode(TreeNode leftNode) {

this.leftNode = leftNode;

}

//设置右儿子

public void setRightNode(TreeNode rightNode) {

this.rightNode = rightNode;

}

//先序遍历

public void frontShow() {

//先遍历当前节点的值

System.out.print(value + " ");

//左节点

if (leftNode != null) {

leftNode.frontShow(); //递归思想

}

//右节点

if (rightNode != null) {

rightNode.frontShow();

}

}

//中序遍历

public void middleShow() {

//左节点

if (leftNode != null) {

leftNode.middleShow(); //递归思想

}

//先遍历当前节点的值

System.out.print(value + " ");

//右节点

if (rightNode != null) {

rightNode.middleShow();

}

}

//后续遍历

public void afterShow() {

//左节点

if (leftNode != null) {

leftNode.afterShow(); //递归思想

}

//右节点

if (rightNode != null) {

rightNode.afterShow();

}

//先遍历当前节点的值

System.out.print(value + " ");

}

//先序查找

public TreeNode frontSearch(int i) {

TreeNode target = null;

//对比当前节点的值

if (this.value == i) {

return this;

//当前节点不是要查找的节点

} else {

//查找左儿子

if (leftNode != null) {

//查找的话t赋值给target,查不到target还是null

target = leftNode.frontSearch(i);

}

//如果target不为空,说明在左儿子中已经找到

if (target != null) {

return target;

}

//如果左儿子没有查到,再查找右儿子

if (rightNode != null) {

target = rightNode.frontSearch(i);

}

}

return target;

}

//删除一个子树

public void delete(int i) {

TreeNode parent = this;

//判断左儿子

if (parent.leftNode != null && parent.leftNode.value == i) {

parent.leftNode = null;

return;

}

//判断右儿子

if (parent.rightNode != null && parent.rightNode.value == i) {

parent.rightNode = null;

return;

}

//如果都不是,递归检查并删除左儿子

parent = leftNode;

if (parent != null) {

parent.delete(i);

}

//递归检查并删除右儿子

parent = rightNode;

if (parent != null) {

parent.delete(i);

}

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

//创建一棵树

BinaryTree binaryTree = new BinaryTree();

//创建一个根节点

TreeNode root = new TreeNode(1);

//把根节点赋给树

binaryTree.setRoot(root);

//创建左,右节点

TreeNode rootLeft = new TreeNode(2);

TreeNode rootRight = new TreeNode(3);

//把新建的节点设置为根节点的子节点

root.setLeftNode(rootLeft);

root.setRightNode(rootRight);

//为第二层的左节点创建两个子节点

rootLeft.setLeftNode(new TreeNode(4));

rootLeft.setRightNode(new TreeNode(5));

//为第二层的右节点创建两个子节点

rootRight.setLeftNode(new TreeNode(6));

rootRight.setRightNode(new TreeNode(7));

//先序遍历

binaryTree.frontShow(); //1 2 4 5 3 6 7

System.out.println();

//中序遍历

binaryTree.middleShow(); //4 2 5 1 6 3 7

System.out.println();

//后序遍历

binaryTree.afterShow(); //4 5 2 6 7 3 1

System.out.println();

//先序查找

TreeNode result = binaryTree.frontSearch(5);

System.out.println(result); //binarytree.TreeNode@1b6d3586

//删除一个子树

binaryTree.delete(2);

binaryTree.frontShow(); //1 3 6 7 ,2和他的子节点被删除了

}

}

7.5 顺序存储的二叉树


在这里插入图片描述

概述:顺序存储使用数组的形式实现;由于非完全二叉树会导致数组中出现空缺,有的位置不能填上数字,所以顺序存储二叉树通常情况下只考虑完全二叉树

原理: 顺序存储在数组中是按照第一层第二层一次往下存储的,遍历方式也有先序遍历、中序遍历、后续遍历

性质

  • 第n个元素的左子节点是:2*n+1;

  • 第n个元素的右子节点是:2*n+2;

  • 第n个元素的父节点是:(n-1)/2

代码实现

  • 树类

public class ArrayBinaryTree {

int[] data;

public ArrayBinaryTree(int[] data) {

this.data = data;

}

//重载先序遍历方法,不用每次传参数了,保证每次从头开始

public void frontShow() {

frontShow(0);

}

//先序遍历

public void frontShow(int index) {

if (data == null || data.length == 0) {

return;

}

//先遍历当前节点的内容

System.out.print(data[index] + " ");

//处理左子树:2*index+1

if (2 * index + 1 < data.length) {

frontShow(2 * index + 1);

}

//处理右子树:2*index+2

if (2 * index + 2 < data.length) {

frontShow(2 * index + 2);

}

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

int[] data = {1,2,3,4,5,6,7};

ArrayBinaryTree tree = new ArrayBinaryTree(data);

//先序遍历

tree.frontShow(); //1 2 4 5 3 6 7

}

}

7.6 线索二叉树(Threaded BinaryTree)


为什么使用线索二叉树?

当用二叉链表作为二叉树的存储结构时,可以很方便的找到某个结点的左右孩子;但一般情况下,无法直接找到该结点在某种遍历序列中的前驱和后继结点

原理:n个结点的二叉链表中含有n+1(2n-(n-1)=n+1个空指针域。利用二叉链表中的空指针域,存放指向结点在某种遍历次序下的前驱和后继结点的指针。

例如:某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱;如果某个结点的右孩子为空,则将空的右孩子指针域改为指向其后继(这种附加的指针称为"线索")

在这里插入图片描述

代码实现

  • 树类

public class ThreadedBinaryTree {

ThreadedNode root;

//用于临时存储前驱节点

ThreadedNode pre = null;

//设置根节点

public void setRoot(ThreadedNode root) {

this.root = root;

}

//中序线索化二叉树

public void threadNodes() {

threadNodes(root);

}

public void threadNodes(ThreadedNode node) {

//当前节点如果为null,直接返回

if (node == null) {

return;

}

//处理左子树

threadNodes(node.leftNode);

//处理前驱节点

if (node.leftNode == null) {

//让当前节点的左指针指向前驱节点

node.leftNode = pre;

//改变当前节点左指针类型

node.leftType = 1;

}

//处理前驱的右指针,如果前驱节点的右指针是null(没有右子树)

if (pre != null && pre.rightNode == null) {

//让前驱节点的右指针指向当前节点

pre.rightNode = node;

//改变前驱节点的右指针类型

pre.rightType = 1;

}

//每处理一个节点,当前节点是下一个节点的前驱节点

pre = node;

//处理右子树

threadNodes(node.rightNode);

}

//遍历线索二叉树

public void threadIterate() {

//用于临时存储当前遍历节点

ThreadedNode node = root;

while (node != null) {

//循环找到最开始的节点

while (node.leftType == 0) {

node = node.leftNode;

}

//打印当前节点的值

System.out.print(node.value + " ");

//如果当前节点的右指针指向的是后继节点,可能后继节点还有后继节点

while (node.rightType == 1) {

node = node.rightNode;

System.out.print(node.value + " ");

}

//替换遍历的节点

node = node.rightNode;

}

}

//获取根节点

public ThreadedNode getRoot() {

return root;

}

//先序遍历

public void frontShow() {

if (root != null) {

root.frontShow();

}

}

//中序遍历

public void middleShow() {

if (root != null) {

root.middleShow();

}

}

//后序遍历

public void afterShow() {

if (root != null) {

root.afterShow();

}

}

//先序查找

public ThreadedNode frontSearch(int i) {

return root.frontSearch(i);

}

//删除一个子树

public void delete(int i) {

if (root.value == i) {

root = null;

} else {

root.delete(i);

}

}

}

  • 节点类

public class ThreadedNode {

//节点的权

int value;

//左儿子

ThreadedNode leftNode;

//右儿子

ThreadedNode rightNode;

//标识指针类型,1表示指向上一个节点,0

int leftType;

int rightType;

public ThreadedNode(int value) {

this.value = value;

}

//设置左儿子

public void setLeftNode(ThreadedNode leftNode) {

this.leftNode = leftNode;

}

//设置右儿子

public void setRightNode(ThreadedNode rightNode) {

this.rightNode = rightNode;

}

//先序遍历

public void frontShow() {

//先遍历当前节点的值

System.out.print(value + " ");

//左节点

if (leftNode != null) {

leftNode.frontShow(); //递归思想

}

//右节点

if (rightNode != null) {

rightNode.frontShow();

}

}

//中序遍历

public void middleShow() {

//左节点

if (leftNode != null) {

leftNode.middleShow(); //递归思想

}

//先遍历当前节点的值

System.out.print(value + " ");

//右节点

if (rightNode != null) {

rightNode.middleShow();

}

}

//后续遍历

public void afterShow() {

//左节点

if (leftNode != null) {

leftNode.afterShow(); //递归思想

}

//右节点

if (rightNode != null) {

rightNode.afterShow();

}

//先遍历当前节点的值

System.out.print(value + " ");

}

//先序查找

public ThreadedNode frontSearch(int i) {

ThreadedNode target = null;

//对比当前节点的值

if (this.value == i) {

return this;

//当前节点不是要查找的节点

} else {

//查找左儿子

if (leftNode != null) {

//查找的话t赋值给target,查不到target还是null

target = leftNode.frontSearch(i);

}

//如果target不为空,说明在左儿子中已经找到

if (target != null) {

return target;

}

//如果左儿子没有查到,再查找右儿子

if (rightNode != null) {

target = rightNode.frontSearch(i);

}

}

return target;

}

//删除一个子树

public void delete(int i) {

ThreadedNode parent = this;

//判断左儿子

if (parent.leftNode != null && parent.leftNode.value == i) {

parent.leftNode = null;

return;

}

//判断右儿子

if (parent.rightNode != null && parent.rightNode.value == i) {

parent.rightNode = null;

return;

}

//如果都不是,递归检查并删除左儿子

parent = leftNode;

if (parent != null) {

parent.delete(i);

}

//递归检查并删除右儿子

parent = rightNode;

if (parent != null) {

parent.delete(i);

}

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

//创建一棵树

ThreadedBinaryTree binaryTree = new ThreadedBinaryTree();

//创建一个根节点

ThreadedNode root = new ThreadedNode(1);

//把根节点赋给树

binaryTree.setRoot(root);

//创建左,右节点

ThreadedNode rootLeft = new ThreadedNode(2);

ThreadedNode rootRight = new ThreadedNode(3);

//把新建的节点设置为根节点的子节点

root.setLeftNode(rootLeft);

root.setRightNode(rootRight);

//为第二层的左节点创建两个子节点

rootLeft.setLeftNode(new ThreadedNode(4));

ThreadedNode fiveNode = new ThreadedNode(5);

rootLeft.setRightNode(fiveNode);

//为第二层的右节点创建两个子节点

rootRight.setLeftNode(new ThreadedNode(6));

rootRight.setRightNode(new ThreadedNode(7));

//中序遍历

binaryTree.middleShow(); //4 2 5 1 6 3 7

System.out.println();

//中序线索化二叉树

binaryTree.threadNodes();

// //获取5的后继节点

// ThreadedNode afterFive = fiveNode.rightNode;

// System.out.println(afterFive.value); //1

binaryTree.threadIterate(); //4 2 5 1 6 3 7

}

}

7.7 二叉排序树(Binary Sort Tree)


无序序列

在这里插入图片描述二叉排序树图解

在这里插入图片描述

概述:二叉排序树(Binary Sort Tree)也叫二叉查找树或者是一颗空树,对于二叉树中的任何一个非叶子节点,要求左子节点比当前节点值小,右子节点比当前节点值大

特点

  • 查找性能与插入删除性能都适中还不错

  • 中序遍历的结果刚好是从大到小

创建二叉排序树原理:其实就是不断地插入节点,然后进行比较。

删除节点

  • 删除叶子节点,只需要找到父节点,将父节点与他的连接断开即可

  • 删除有一个子节点的就需要将他的子节点换到他现在的位置

  • 删除有两个子节点的节点,需要使用他的前驱节点或者后继节点进行替换,就是左子树最右下方的数(最大的那个)或右子树最左边的树(最小的数);即离节点值最接近的值;(还要注解要去判断这个值有没有右节点,有就要将右节点移上来)

代码实现

  • 树类

public class BinarySortTree {

Node root;

//添加节点

public void add(Node node) {

//如果是一颗空树

if (root == null) {

root = node;

} else {

root.add(node);

}

}

//中序遍历

public void middleShow() {

if (root != null) {

root.middleShow(root);

}

}

//查找节点

public Node search(int value) {

if (root == null) {

return null;

}

return root.search(value);

}

//查找父节点

public Node searchParent(int value) {

if (root == null) {

return null;

}

return root.searchParent(value);

}

//删除节点

public void delete(int value) {

if (root == null) {

return;

} else {

//找到这个节点

Node target = search(value);

//如果没有这个节点

if (target == null) {

return;

}

//找到他的父节点

Node parent = searchParent(value);

//要删除的节点是叶子节点

if (target.left == null && target.left == null) {

//要删除的节点是父节点的左子节点

if (parent.left.value == value) {

parent.left = null;

}

//要删除的节点是父节点的右子节点

else {

parent.right = null;

}

}

//要删除的节点有两个子节点的情况

else if (target.left != null && target.right != null) {

//删除右子树中值最小的节点,并且获取到值

int min = deletMin(target.right);

//替换目标节点中的值

target.value = min;

}

//要删除的节点有一个左子节点或右子节点

else {

//有左子节点

if (target.left != null) {

//要删除的节点是父节点的左子节点

if (parent.left.value == value) {

parent.left = target.left;

}

//要删除的节点是父节点的右子节点

else {

parent.right = target.left;

}

}

//有右子节点

else {

//要删除的节点是父节点的左子节点

if (parent.left.value == value) {

parent.left = target.right;

}

//要删除的节点是父节点的右子节点

else {

parent.right = target.right;

}

}

}

}

}

//删除一棵树中最小的节点

private int deletMin(Node node) {

Node target = node;

//递归向左找最小值

while (target.left != null) {

target = target.left;

}

//删除最小的节点

delete(target.value);

return target.value;

}

}

  • 节点类

public class Node {

int value;

Node left;

Node right;

public Node(int value) {

this.value = value;

}

//向子树中添加节点

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);

}

}

}

//中序遍历二叉排序树,结果刚好是从小到大

public void middleShow(Node node) {

if (node == null) {

return;

}

middleShow(node.left);

System.out.print(node.value + " ");

middleShow(node.right);

}

//查找节点

public Node search(int value) {

if (this.value == value) {

return this;

} else if (value < this.value) {

if (left == null) {

return null;

}

return left.search(value);

} else {

if (right == null) {

return null;

}

return right.search(value);

}

}

//查找父节点

public Node searchParent(int value) {

if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {

return this;

} else {

if (this.value > value && this.left != null) {

return this.left.searchParent(value);

} else if (this.value < value && this.right != null) {

return this.right.searchParent(value);

}

return null;

}

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

int[] arr = {8, 3, 10, 1, 6, 14, 4, 7, 13};

//创建一颗二叉排序树

BinarySortTree bst = new BinarySortTree();

//循环添加

/* for(int i=0;i< arr.length;i++) {

bst.add(new Node(arr[i]));

}*/

for (int i : arr) {

bst.add(new Node(i));

}

//中序遍历

bst.middleShow(); //1 3 4 6 7 8 10 13 14

System.out.println();

//查找节点

Node node = bst.search(10);

System.out.println(node.value);//10

Node node2 = bst.search(20);

System.out.println(node2); //null

//查找父节点

Node node3 = bst.searchParent(1);

Node node4 = bst.searchParent(14);

System.out.println(node3.value); //3

System.out.println(node4.value); //10

//删除叶子节点

// bst.delete(13);

// bst.middleShow(); //1 3 4 6 7 8 10 14

// System.out.println();

// //删除只有一个子节点的节点

// bst.delete(10);

// bst.middleShow(); //1 3 4 6 7 8 ;10和14都没了

//删除有两个子节点的节点

bst.delete(3);

bst.middleShow(); //1 4 6 7 8 10 13 14

}

}

7.8 平衡二叉树( Balanced Binary Tree)


为什么使用平衡二叉树?

平衡二叉树(Balanced Binary Tree)又被称为AVL树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

二叉排序树插入 {1,2,3,4,5,6} 这种数据结果如下图所示:

在这里插入图片描述

平衡二叉树插入 {1,2,3,4,5,6} 这种数据结果如下图所示:

在这里插入图片描述

如何判断平衡二叉树?

  • 1、是二叉排序树

  • 2、任何一个节点的左子树或者右子树都是平衡二叉树(左右高度差小于等于 1)

(1)下图不是平衡二叉树,因为它不是二叉排序树违反第 1 条件

在这里插入图片描述

(2)下图不是平衡二叉树,因为有节点子树高度差大于 1 违法第 2 条件(5的左子树为0,右子树为2)

在这里插入图片描述

(3)下图是平衡二叉树,因为符合 1、2 条件

在这里插入图片描述

相关概念

平衡因子 BF

  • 定义:左子树和右子树高度差

  • 计算:左子树高度 - 右子树高度的值

  • 别名:简称 BF(Balance Factor)

  • 一般来说 BF 的绝对值大于 1,,平衡树二叉树就失衡,需要旋转纠正

最小不平衡子树

  • 距离插入节点最近的,并且 BF 的绝对值大于 1 的节点为根节点的子树。

  • 旋转纠正只需要纠正最小不平衡子树即可

  • 例子如下图所示:

在这里插入图片描述

旋转方式

2 种旋转方式:

左旋 :

  • 旧根节点为新根节点的左子树

  • 新根节点的左子树(如果存在)为旧根节点的右子树

右旋:

  • 旧根节点为新根节点的右子树

  • 新根节点的右子树(如果存在)为旧根节点的左子树

4 种旋转纠正类型

  • 左左型:插入左孩子的左子树,右旋

  • 右右型:插入右孩子的右子树,左旋

  • 左右型:插入左孩子的右子树,先左旋,再右旋

  • 右左型:插入右孩子的左子树,先右旋,再左旋

在这里插入图片描述

左左型

第三个节点(1)插入的时候,BF(3) = 2,BF(2) = 1,右旋,根节点顺时针旋转

在这里插入图片描述

右右型

第三个节点(3)插入的时候,BF(1)=-2 BF(2)=-1,RR 型失衡,左旋,根节点逆时针旋转

在这里插入图片描述

左右型

第三个节点(3)插入的 时候,BF(3)=2 BF(1)=-1 LR 型失衡,先 左旋 再 右旋

在这里插入图片描述

在这里插入图片描述

右左型

第三个节点(1)插入的 时候,BF(1)=-2 BF(3)=1 RL 型失衡,先 右旋 再 左旋

在这里插入图片描述

在这里插入图片描述

实例

(1)、依次插入 3、2、1 插入第三个点 1 的时候 BF(3)=2 BF(2)=1,LL 型失衡,对最小不平衡树 {3,2,1}进行 右旋

  • 旧根节点(节点 3)为新根节点(节点 2)的右子树

  • 新根节点(节点 2)的右子树(这里没有右子树)为旧根节点的左子树

在这里插入图片描述

(2)依次插入 4 ,5 插入 5 点的时候 BF(3) = -2 BF(4)=-1,RR 型失衡,对最小不平衡树 {3,4,5} 进行左旋

  • 旧根节点(节点 3)为新根节点(节点 4)的左子树

  • 新根节点(节点 4)的左子树(这里没有左子树)为旧根节点的右子树

在这里插入图片描述

(3)插入 4 ,5 插入 5 点的时候 BF(2)=-2 BF(4)=-1 ,RR 型失衡 对最小不平衡树{1,2,4}进行左旋

  • 旧根节点(节点 2)为新根节点(节点 4)的左子树

在这里插入图片描述

  • 新根节点(节点 4)的 左子树(节点 3)为旧根节点的右子树

在这里插入图片描述

(4)插入 7 节点的时候 BF(5)=-2, BF(6)=-1 ,RR 型失衡,对最小不平衡树 进行左旋

  • 旧根节点(节点 5)为新根节点(节点 6)的左子树

  • 新根节点的左子树(这里没有)为旧根节点的右子树

在这里插入图片描述

(5)依次插入 10 ,9 。插入 9 点的时候 BF(10) = 1,BF(7) = -2,RL 型失衡,对先右旋再左旋,右子树先右旋

  • 旧根节点(节点 10)为新根节点(节点 9)的右子树

  • 新根节点(节点 9)的右子树(这里没有右子树)为旧根节点的左子树

在这里插入图片描述

最小不平衡子树再左旋:

  • 旧根节点(节点 7)为新根节点(节点 9)的左子树

  • 新根节点(节点 9)的左子树(这里没有左子树)为旧根节点的右子树

在这里插入图片描述

代码实现

在这里插入图片描述

  • 节点类

public class Node {

int value;

Node left;

Node right;

public Node(int value) {

this.value = value;

}

//获取当前节点高度

public int height() {

return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;

}

//获取左子树高度

public int leftHeight() {

if (left == null) {

return 0;

}

return left.height();

}

//获取右子树高度

public int rightHeight() {

if (right == null) {

return 0;

}

return right.height();

}

//向子树中添加节点

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 (leftHeight() - rightHeight() >= 2) {

//双旋转,当左子树左边高度小于左子树右边高度时

if (left != null && left.leftHeight() < left.rightHeight()) {

//左子树先进行左旋转

left.leftRotate();

//整体进行右旋转

rightRotate();

}

//单旋转

else {

rightRotate();

}

}

//左旋转

if (leftHeight() - rightHeight() <= -2) {

//双旋转

if (right != null && right.rightHeight() < right.leftHeight()) {

right.rightRotate();

leftRotate();

}

//单旋转

else {

leftRotate();

}

}

}

//右旋转

private void rightRotate() {

//创建一个新的节点,值等于当前节点的值

Node newRight = new Node(value);

//把新节点的右子树设置为当前节点的右子树

newRight.right = right;

//把新节点的左子树设置为当前节点的左子树的右子树

newRight.left = left.right;

//把当前节点的值换位左子节点的值

value = left.value;

//把当前节点的左子树设置为左子树的左子树

left = left.left;

//把当前节点设置为新节点

right = newRight;

}

//左旋转

private void leftRotate() {

//创建一个新的节点,值等于当前节点的值

Node newLeft = new Node(value);

//把新节点的左子树设置为当前节点的左子树

newLeft.left = left;

//把新节点的右子树设置为当前节点的右子树的左子树

newLeft.right = right.left;

//把当前节点的值换位右子节点的值

value = right.value;

//把当前节点的右子树设置为右子树的右子树

right = right.right;

//把当前节点设置为新节点

left = newLeft;

}

//中序遍历二叉排序树,结果刚好是从小到大

public void middleShow(Node node) {

if (node == null) {

return;

}

middleShow(node.left);

System.out.print(node.value + " ");

middleShow(node.right);

}

//查找节点

public Node search(int value) {

if (this.value == value) {

return this;

} else if (value < this.value) {

if (left == null) {

return null;

}

return left.search(value);

} else {

if (right == null) {

return null;

}

return right.search(value);

}

}

//查找父节点

public Node searchParent(int value) {

if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) {

return this;

} else {

if (this.value > value && this.left != null) {

return this.left.searchParent(value);

} else if (this.value < value && this.right != null) {

return this.right.searchParent(value);

}

return null;

}

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

int[] arr = {1,2,3,4,5,6};

//创建一颗二叉排序树

BinarySortTree bst = new BinarySortTree();

//循环添加

for (int i : arr) {

bst.add(new Node(i));

}

//查看高度

System.out.println(bst.root.height()); //3

//查看节点值

System.out.println(bst.root.value); //根节点为4

System.out.println(bst.root.left.value); //左子节点为2

System.out.println(bst.root.right.value); //右子节点为5

}

}

第8章 赫夫曼树

==========================================================================

8.1 赫夫曼树概述


HuffmanTree因为翻译不同所以有其他的名字:赫夫曼树、霍夫曼树、哈夫曼树

赫夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1_L1+W2_L2+W3_L3+…+Wn_Ln),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明赫夫曼树的WPL是最小的。

8.2 赫夫曼树定义


路径: 路径是指从一个节点到另一个节点的分支序列。

路径长度: 指从一个节点到另一个结点所经过的分支数目。 如下图:从根节点到a的分支数目为2

在这里插入图片描述

树的路径长度: 树中所有结点的路径长度之和为树的路径长度PL。 如下图:PL为10

在这里插入图片描述

节点的权: 给树的每个结点赋予一个具有某种实际意义的实数,我们称该实数为这个结点的权。如下图:7、5、2、4

在这里插入图片描述

带权路径长度: 从树根到某一结点的路径长度与该节点的权的乘积,叫做该结点的带权路径长度。如下图:A的带权路径长度为2*7=14

在这里插入图片描述

树的带权路径长度(WPL): 树的带权路径长度为树中所有叶子节点的带权路径长度之和

最优二叉树:权值最大的节点离跟节点越近的二叉树,所得WPL值最小,就是最优二叉树。如下图:(b)

在这里插入图片描述

  • (a)WPL=9*2+4*2+5*2+2*2=40

  • (b)WPL=9*1+5*2+4*3+2*3=37

  • (c) WPL=4*1+2*2+5*3+9*3=50

8.3 构造赫夫曼树步骤


对于数组{5,29,7,8,14,23,3,11},我们把它构造成赫夫曼树

第一步:使用数组中所有元素创建若干个二叉树,这些值作为节点的权值(只有一个节点)。

在这里插入图片描述

第二步:将这些节点按照权值的大小进行排序。

在这里插入图片描述

第三步:取出权值最小的两个节点,并创建一个新的节点作为这两个节点的父节点,这个父节点的权值为两个子节点的权值之和。将这两个节点分别赋给父节点的左右节点

在这里插入图片描述

第四步:删除这两个节点,将父节点添加进集合里

在这里插入图片描述

第五步:重复第二步到第四步,直到集合中只剩一个元素,结束循环

在这里插入图片描述

8.4 代码实现


  • 节点类

//接口实现排序功能

public class Node implements Comparable {

int value;

Node left;

Node right;

public Node(int value) {

this.value = value;

}

@Override

public int compareTo(Node o) {

return -(this.value - o.value); //集合倒叙,从大到小

}

@Override

public String toString() {

return “Node value=” + value ;

}

}

  • 测试类

import java.util.ArrayList;

import java.util.Collections;

import java.util.List;

public class Demo {

public static void main(String[] args) {

int[] arr = {5, 29, 7, 8, 14, 23, 3, 11};

Node node = createHuffmanTree(arr);

System.out.println(node); //Node value=100

}

//创建赫夫曼树

public static Node createHuffmanTree(int[] arr) {

//使用数组中所有元素创建若干个二叉树(只有一个节点)

List nodes = new ArrayList<>();

for (int value : arr) {

nodes.add(new Node(value));

}

//循环处理

while (nodes.size() > 1) {

//排序

Collections.sort(nodes);

//取出最小的两个二叉树(集合为倒叙,从大到小)

Node left = nodes.get(nodes.size() - 1); //权值最小

Node right = nodes.get(nodes.size() - 2); //权值次小

//创建一个新的二叉树

Node parent = new Node(left.value + right.value);

//删除原来的两个节点

nodes.remove(left);

nodes.remove(right);

//新的二叉树放入原来的二叉树集合中

nodes.add(parent);

//打印结果

System.out.println(nodes);

}

return nodes.get(0);

}

}

  • 循环次数结果

[Node value=29, Node value=23, Node value=14, Node value=11, Node value=8, Node value=7, Node value=8]

[Node value=29, Node value=23, Node value=14, Node value=11, Node value=8, Node value=15]

[Node value=29, Node value=23, Node value=15, Node value=14, Node value=19]

[Node value=29, Node value=23, Node value=19, Node value=29]

[Node value=29, Node value=29, Node value=42]

[Node value=42, Node value=58]

[Node value=100]

Node value=100

Process finished with exit code 0

第9章 多路查找树(2-3树、2-3-4树、B树、B+树)

===============================================================================================

9.1 为什么使用多路查找树


二叉树存在的问题

二叉树需要加载到内存的,如果二叉树的节点少,没有什么问题,但是如果二叉树的节点很多(比如1亿), 就存在如下问题:

  • 问题1:在构建二叉树时,需要多次进行I/O操作(海量数据存在数据库或文件中),节点海量,构建二叉树时,速度有影响.

  • 问题2:节点海量,也会造成二叉树的高度很大,会降低操作速度.

在这里插入图片描述

解决上述问题 —> 多叉树

多路查找树

  • 1、在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)

  • 2、后面我们讲解的"2-3树","2-3-4树"就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。

  • 3、举例说明(下面2-3树就是一颗多叉树)

在这里插入图片描述

9.2 2-3树


2-3树定义

  • 所有叶子节点都要在同一层

  • 二节点要么有两个子节点,要么没有节点

  • 三节点要么没有节点,要么有三个子节点

  • 不能出现节点不满的情况

在这里插入图片描述

2-3树插入的操作

插入原理

对于2-3树的插入来说,与平衡二叉树相同,插入操作一定是发生在叶子节点上,并且节点的插入和删除都有可能导致不平衡的情况发生,在插入和删除节点时也是需要动态维持平衡的,但维持平衡的策略和AVL树是不一样的。

AVL树向下添加节点之后通过旋转来恢复平衡,而2-3树是通过节点向上分裂来维持平衡的,也就是说2-3树插入元素的过程中层级是向上增加的,因此不会导致叶子节点不在同一层级的现象发生,也就不需要旋转了。

三种插入情况

1)对于空树,插入一个2节点即可;

2)插入节点到一个2节点的叶子上。由于本身就只有一个元素,所以只需要将其升级为3节点即可(如:插入3)。

在这里插入图片描述

3)插入节点到一个3节点的叶子上。因为3节点本身最大容量,因此需要拆分,且将树中两元素或者插入元素的三者中选择其一向上移动一层。

分为三种情况:

  • 升级父节点(插入5)

在这里插入图片描述

  • 升级根节点(插入11)

在这里插入图片描述

  • 增加树高度(插入2,从下往上拆)

在这里插入图片描述

2-3树删除的操作

删除原理:2-3树的删除也分为三种情况,与插入相反。

三种删除情况

1)所删元素位于一个3节点的叶子节点上,直接删除,不会影响树结构(如:删除9)

在这里插入图片描述

2)所删元素位于一个2节点上,直接删除,破坏树结构

在这里插入图片描述

分为四种情况:

  • 此节点双亲也是2节点,且拥有一个3节点的右孩子(如:删除1)

在这里插入图片描述

  • 此节点的双亲是2节点,它右孩子也是2节点(如:删除4)

在这里插入图片描述

  • 此节点的双亲是3节点(如:删除10)

在这里插入图片描述

  • 当前树是一个满二叉树,降低树高(如:删除8)

在这里插入图片描述

3)所删元素位于非叶子的分支节点。此时按树中序遍历得到此元素的前驱或后续元素,补位

两种情况:

  • 分支节点是2节点(如:删除4)

在这里插入图片描述

  • 分支节点是3节点(如:删除12)

在这里插入图片描述

9.3 2-3-4树


2-3-4树是2-3树的扩展,包括了 4 节点的使用,一个 4 节点包含小中大三个元素和四个孩子(或没有孩子)

2-3-4树的插入操作

1)如果待插入的节点不是 4 节点,则直接插入即可

2)如果待插入的节点是 4 节点,则先把新节点临时插入进去变成 5 节点,然后对 5 节点进行向上分裂、合并,5 节点分裂成两个 2 节点(5 节点最小的元素、5 节点第二个元素)、1个 3 节点(5 节点后两个元素),然后将分裂之后的第2个 2 节点向上合并到父节点中,然后把父节点作为插入元素之后的当前节点,重复(1)、(2)步骤,直到满足2-3-4树的定义性质

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2-3-4树的删除操作

删除顺序使1,6,3,4,5,2,9

在这里插入图片描述

9.4 B树


B树(BTree)是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。

我们把结点最大的孩子树目称为B树的阶,因此,2-3树是3阶B树,2-3-4树是4阶B树

在这里插入图片描述

如下图,比如说要查找7,首先从外存读取得到根节点3,5,8三个元素,发现7不在,但是5、8之间,因此就通过A2再读取外存的2,6,7节点找到结束。

在这里插入图片描述

B树的数据结构为内外存的数据交互准备的。当要处理的数据很大时,无法一次全部装入内存。这时对B树调整,使得B树的阶数与硬盘存储的页面大小相匹配。比如说一棵B树的阶为1001(即1个节点包含1000个关键字),高度为2(从0开始),它可以存储超过10亿个关键字(1001x1001x1000+1001x1000+1000),只要让根节点持久的保留在内存中,那么在这颗树上,寻找某一个关键字至多需要两次硬盘的读取即可。

对于n个关键字的m阶B树,最坏情况查找次数计算

第一层至少1个节点,第二层至少2个节点,由于除根节点外每个分支节点至少有⌈m/2⌉棵子树,则第三层至少有2x⌈m/2⌉个节点。。。这样第k+1层至少有2x(⌈m/2⌉)^(k-1),实际上,k+1层的节点就是叶子节点。若m阶B树有n个关键字,那么当你找到叶子节点,其实也就等于查找不成功的节点为n+1,因此

n+1>=2x(⌈m/2⌉)^(k-1),即

在这里插入图片描述

在含有n个关键字的B树上查找时,从根节点到关键字节点的路径上涉及的节点数不超多在这里插入图片描述

9.5 B+树


B+树可以说是B树的升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。大部分文件系统和数据均采用B+树来实现索引结构。

下图B树,我们要遍历它,假设每个节点都属于硬盘的不同页面,我们为了中序遍历所有的元素,页面2-页面1-页面3-页面1-页面4-页面1-页面5,页面1遍历了3次,而且我们每经过节点遍历时,都会对节点中的元素进行一次遍历

在这里插入图片描述

B+树是应文件系统所需而出的一种B树的变形树,在B树中,每一个元素树中只出现一次,而B+树中,出现在分支节点中的元素会被当做他们在该分支节点位置的中序后继者(叶子节点)中再次列出。另外,每一个叶子节点都会保存一个指向后一叶子节点的指针。

下图就是B+树,灰色关键字,在根节点出现,在叶子节点中再次列出

在这里插入图片描述

一棵m阶的B+树和m阶的B树的差异在于

  • 有n棵子树的非叶节点中包含有n个关键字(B树中是n-1个关键字),但是每个关键字不保存数据,只用来保存叶子节点相同关键字的索引,所有数据都保存在叶子节点。(此处,对于非叶节点的m颗子树和n个关键字的关系,mysql的索引结构似乎是m=n+1,而不是上面的m=n)

  • 所有的非叶节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

  • 所有的叶子节点包含全部关键字的信息,及指向含这些关键字所指向的具体磁盘记录的指针data,并且每一个叶子节点带有指向下一个相邻的叶节点指针,形成链表

9.6 总结


  • B树的非叶节点会存储关键字及其对应的数据地址,B+树的非叶节点只存关键字索引,不会存具体的数据地址,因此B+树的一个节点相比B树能存储更多的索引元素,一次性读入内存的需要查找的关键字也就越多,B+树的高度更小,相对IO读写次数就降低了。

  • B树的查询效率并不稳定,最好的情况只查询一次(根节点),最坏情况是查找到叶子节点,而B+树由于非叶节点不存数据地址,而只是叶子节点中关键字的索引。所有查询都要查找到叶子节点才算命中,查询效率比较稳定。这对于sql语句的优化是非常有帮助的。

  • B+树所有叶子节点形成有序链表,只需要去遍历叶子节点就可以实现整棵树的遍历。方便数据的范围查询,而B树不支持这样的操作或者说效率太低。

  • 现代数据库和文件系统的索引表大部分是使用B+树来实现的,但并不是全部

第10章 图结构

==========================================================================

10.1 图的基本概念


(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的。

常用术语

| 术语 | 含义 |

| — | — |

| 顶点 | 图中的某个结点 |

| 边 | 顶点之间连线 |

| 相邻顶点 | 由同一条边连接在一起的顶点 |

| 度 | 一个顶点的相邻顶点个数 |

| 简单路径 | 由一个顶点到另一个顶点的路线,且没有重复经过顶点 |

| 回路 | 出发点和结束点都是同一个顶点 |

| 无向图 | 图中所有的边都没有方向 |

| 有向图 | 图中所有的边都有方向 |

| 无权图 | 图中的边没有权重值 |

| 有权图 | 图中的边带有一定的权重值 |

图的结构很简单,就是由顶点 V 集和边 E 集构成,因此图可以表示成 G = (V,E)

无向图

若顶点 Vi 到 Vj 之间的边没有方向,则称这条边为无向边 (Edge) ,用无序偶对 (Vi,Vj) 来表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图 (Undirected graphs)

如:下图就是一个无向图,由于是无方向的,连接顶点 A 与 D 的边,可以表示无序队列(A,D),也可以写成 (D,A),但不能重复。顶点集合 V = {A,B,C,D};边集合 E = {(A,B),(A,D),(A,C)(B,C),(C,D),}

在这里插入图片描述

有向图

用有序偶<Vi,Vj>来表示,Vi 称为弧尾 (Tail) , Vj称为弧头 (Head)。 如果图中任意两个顶点之间的边都是有向边,则称该图为有向图 (Directed grahs)

如:下图就是一个有向图。连接顶点 A 到 D 的有向边就是弧,A是弧尾,D 是弧头, <A, D>表示弧, 注意不能写成<D,A>。其中顶点集合 V = { A,B,C,D}; 弧集合 E = {<A,D>,<B,A>,<B,C>,<C,A>}

在这里插入图片描述

注意:无向边用小括号 “()” 表示,而有向边则是用尖括号"<>"表示

有权图

有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight) 。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网 (Network)。

如下图

在这里插入图片描述

10.2 图的存储结构及实现


图结构的常见的两个存储方式: 邻接矩阵 、邻接表

邻接矩阵

在这里插入图片描述图中的 0 表示该顶点无法通向另一个顶点,相反 1 就表示该顶点能通向另一个顶点

先来看第一行,该行对应的是顶点A,那我们就拿顶点A与其它点一一对应,发现顶点A除了不能通向顶点D和自身,可以通向其它任何一个的顶点

因为该图为无向图,因此顶点A如果能通向另一个顶点,那么这个顶点也一定能通向顶点A,所以这个顶点对应顶点A的也应该是 1

虽然我们确实用邻接矩阵表示了图结构,但是它有一个致命的缺点,那就是矩阵中存在着大量的 0,这在程序中会占据大量的内存。此时我们思考一下,0 就是表示没有,没有为什么还要写,所以我们来看一下第二种表示图结构的方法,它就很好的解决了邻接矩阵的缺陷

代码实现

  • 顶点类

public class Vertex {

private String value;

public Vertex(String value) {

this.value = value;

}

public String getValue() {

return value;

}

public void setValue(String value) {

this.value = value;

}

@Override

public String toString() {

return value;

}

}

  • 图类

public class Graph {

private Vertex[] vertex; //顶点数组

private int currentSize; //默认顶点位置

public int[][] adjMat; //邻接表

public Graph(int size) {

vertex = new Vertex[size];

adjMat = new int[size][size];

}

//向图中加入顶点

public void addVertex(Vertex v) {

vertex[currentSize++] = v;

}

//添加边

public void addEdge(String v1, String v2) {

//找出两个点的下标

int index1 = 0;

for (int i = 0; i < vertex.length; i++) {

if (vertex[i].getValue().equals(v1)) {

index1 = i;

break;

}

}

int index2 = 0;

for (int i = 0; i < vertex.length; i++) {

if (vertex[i].getValue().equals(v2)) {

index2 = i;

break;

}

}

//表示两个点互通

adjMat[index1][index2] = 1;

adjMat[index2][index1] = 1;

}

}

  • 测试类

public class Demo {

public static void main(String[] args) {

Vertex v1 = new Vertex(“A”);

Vertex v2 = new Vertex(“B”);

Vertex v3 = new Vertex(“C”);

Vertex v4 = new Vertex(“D”);

Vertex v5 = new Vertex(“E”);

Graph g = new Graph(5);

g.addVertex(v1);

g.addVertex(v2);

g.addVertex(v3);

g.addVertex(v4);

g.addVertex(v5);

//增加边

g.addEdge(“A”, “B”);

g.addEdge(“A”, “C”);

g.addEdge(“A”, “E”);

g.addEdge(“C”, “E”);

g.addEdge(“C”, “D”);

for (int[] a : g.adjMat) {

System.out.println(Arrays.toString(a));

}

}

}

  • 结果值

[0, 1, 1, 0, 1]

[1, 0, 0, 0, 0]

[1, 0, 0, 1, 1]

[0, 0, 1, 0, 0]

[1, 0, 1, 0, 0]

邻接表

邻接表 是由每个顶点以及它的相邻顶点组成的

在这里插入图片描述

如上图:图中最左侧红色的表示各个顶点,它们对应的那一行存储着与它相关联的顶点

  • 顶点A 与 顶点B 、顶点C 、顶点E 相关联

  • 顶点B 与 顶点A 相关联

  • 顶点C 与 顶点A 、顶点D 、顶点E 相关联

  • 顶点D 与 顶点C 相关联

  • 顶点E 与 顶点A 、顶点C 相关联

10.3 图的遍历方式及实现


从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历

在图结构中,存在着两种遍历搜索的方式,分别是 广度优先搜索深度优先搜索

广度优先搜索

广度优先遍历(BFS):类似于图的层次遍历,它的基本思想是:首先访问起始顶点v,然后选取v的所有邻接点进行访问,再依次对v的邻接点相邻接的所有点进行访问,以此类推,直到所有顶点都被访问过为止

BFS和树的层次遍历一样,采取队列实现,这里添加一个标记数组,用来标记遍历过的顶点

在这里插入图片描述

执行步骤

  • 1、先把 A 压入队列,然后做出队操作,A 出队

  • 2、把 A 直接相关的顶点 ,B、F 做入队操作

  • 3、B 做出队操作,B 相关的点 C、I、G 做入队操作

  • 4、F 做出队操作,F 相关的点 E 做入队操作

  • 5、C 做出队操作,C 相关的点 D 做入队操作

  • 6、I 做出队操作(I 相关的点B、C、D 都已经做过入队操作了,不能重复入队)

  • 7、G 做出队操作,G 相关的点 H 做入队操作

  • 8、E 做出队操作…

  • 9、D 做出队操作…

  • 10、H 做出队操作,没有元素了

代码实现

深度优先搜索

深度优先遍历(DFS):从一个顶点开始,沿着一条路径一直搜索,直到到达该路径的最后一个结点,然后回退到之前经过但未搜索过的路径继续搜索,直到所有路径和结点都被搜索完毕

DFS与二叉树的先序遍历类似,可以采用递归或者栈的方式实现

在这里插入图片描述

执行步骤

  • 1、从 1 出发,路径为:1 -> 2 -> 3 -> 6 -> 9 -> 8 -> 5 -> 4

  • 2、当搜索到 4 时,相邻没有发现未被访问的点,此时我们要往后倒退,找寻别的没搜索过的路径

  • 3、退回到 5 ,相邻没有发现未被访问的点,继续后退

  • 4、退回到 8 ,相邻发现未被访问的点 7,路径为:8 -> 7

  • 5、当搜索到 7 ,相邻没有发现未被访问的点,,此时我们要往后倒退…

  • 6、退回路径7 -> 8 -> 9 -> 6 -> 3 -> 2 -> 1,流程结束

代码实现

  • 栈类

public class MyStack {

//栈的底层使用数组来存储数据

//private int[] elements;

int[] elements; //测试时使用

public MyStack() {

elements = new int[0];

}

//添加元素

public void push(int element) {

//创建一个新的数组

int[] newArr = new int[elements.length + 1];

//把原数组中的元素复制到新数组中

for (int i = 0; i < elements.length; i++) {

newArr[i] = elements[i];

}

//把添加的元素放入新数组中

newArr[elements.length] = element;

//使用新数组替换旧数组

elements = newArr;

}

//取出栈顶元素

public int pop() {

//当栈中没有元素

if (is_empty()) {

throw new RuntimeException(“栈空”);

}

//取出数组的最后一个元素

int element = elements[elements.length - 1];

//创建一个新数组

int[] newArr = new int[elements.length - 1];

//原数组中除了最后一个元素其他元素放入新数组

for (int i = 0; i < elements.length - 1; i++) {

newArr[i] = elements[i];

}

elements = newArr;

return element;

}

//查看栈顶元素

public int peek() {

return elements[elements.length - 1];

}

//判断栈是否为空

public boolean is_empty() {

return elements.length == 0;

}

//查看栈的元素个数

public int size() {

return elements.length;

}

}

  • 顶点类

public class Vertex {

private String value;

public boolean visited; //访问状态

public Vertex(String value) {

super();

this.value = value;

}

public String getValue() {

return value;

}

public void setValue(String value) {

this.value = value;

}

@Override

public String toString() {

return value;

}

}

  • 图类

import mystack.MyStack;

public class Graph {

private Vertex[] vertex; //顶点数组

private int currentSize; //默认顶点位置

public int[][] adjMat; //邻接表

private MyStack stack = new MyStack(); //栈

private int currentIndex; //当前遍历的下标

public Graph(int size) {

vertex = new Vertex[size];

adjMat = new int[size][size];

}

//向图中加入顶点

public void addVertex(Vertex v) {

vertex[currentSize++] = v;

}

//添加边

public void addEdge(String v1, String v2) {

//找出两个点的下标

int index1 = 0;

for (int i = 0; i < vertex.length; i++) {

if (vertex[i].getValue().equals(v1)) {

index1 = i;

break;

}

}

int index2 = 0;

for (int i = 0; i < vertex.length; i++) {

if (vertex[i].getValue().equals(v2)) {

index2 = i;

break;

}

}

//表示两个点互通

adjMat[index1][index2] = 1;

adjMat[index2][index1] = 1;

}

//深度优先搜索

public void dfs() {

//把第0个顶点标记为已访问状态

vertex[0].visited = true;

//把第0个的下标放入栈中

stack.push(0);

//打印顶点值

System.out.println(vertex[0].getValue());

//遍历

out:

while (!stack.is_empty()) {

for (int i = currentIndex + 1; i < vertex.length; i++) {

//如果和下一个遍历的元素是通的

if (adjMat[currentIndex][i] == 1 && vertex[i].visited == false) {

//把下一个元素压入栈中

stack.push(i);

vertex[i].visited = true;

System.out.println(vertex[i].getValue());

continue out;

}

}

//弹出栈顶元素(往后退)

stack.pop();

//修改当前位置为栈顶元素的位置

if (!stack.is_empty()) {

currentIndex = stack.peek();

}

}

}

}

  • 测试类

import java.util.Arrays;

public class Demo {

public static void main(String[] args) {

Vertex v1 = new Vertex(“A”);

Vertex v2 = new Vertex(“B”);

Vertex v3 = new Vertex(“C”);

Vertex v4 = new Vertex(“D”);

Vertex v5 = new Vertex(“E”);

Graph g = new Graph(5);

g.addVertex(v1);

g.addVertex(v2);

g.addVertex(v3);

g.addVertex(v4);

g.addVertex(v5);

//增加边

g.addEdge(“A”, “B”);

g.addEdge(“A”, “C”);

g.addEdge(“A”, “E”);

g.addEdge(“C”, “E”);

g.addEdge(“C”, “D”);

for (int[] a : g.adjMat) {

System.out.println(Arrays.toString(a));

}

//深度优先遍历

g.dfs();

// A

// B

// C

// E

// D

}

}

第11章 冒泡排序(含改进版)

=================================================================================

11.1 冒泡排序概念


冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

运行流程

  • 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  • 针对所有的元素重复以上的步骤,除了最后一个。

  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

动图实现

在这里插入图片描述

静图详解

交换过程图示(第一次):

在这里插入图片描述那么我们需要进行n-1次冒泡过程,每次对应的比较次数如下图所示:

在这里插入图片描述

11.2 代码实现


import java.util.Arrays;

public class BubbleSort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

bubbleSort(arr);

// [4, 5, 3, 2, 1, 6]

// [4, 3, 2, 1, 5, 6]

// [3, 2, 1, 4, 5, 6]

// [2, 1, 3, 4, 5, 6]

// [1, 2, 3, 4, 5, 6]

}

//冒泡排序,共需要比较length-1轮

public static void bubbleSort(int[] arr) {

//控制一共比较多少轮

for (int i = 0; i < arr.length - 1; i++) {

//控制比较次数

for (int j = 0; j < arr.length - 1 - i; j++) {

if (arr[j] > arr[j + 1]) {

int temp = arr[j];

arr[j] = arr[j + 1];

arr[j + 1] = temp;

}

}

//打印每次排序后的结果

System.out.println(Arrays.toString(arr));

}

}

}

11.3 时间复杂度


  • 最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)

  • 最坏时间复杂度:O(n^2)

  • 稳定性:稳定

排序分析:待排数组中一共有6个数,第一轮排序时进行了5次比较,第二轮排序时进行了4比较,依次类推,最后一轮进行了1次比较。

数组元素总数为N时,则一共需要的比较次数为:(N-1)+ (N-2)+ (N-3)+ ...1=N*(N-1)/2

算法约做了N^2/2次比较。因为只有在前面的元素比后面的元素大时才交换数据,所以交换的次数少于比较的次数。如果数据是随机的,大概有一半数据需要交换,则交换的次数为N^2/4(不过在最坏情况下,即初始数据逆序时,每次比较都需要交换)。

交换和比较的操作次数都与 N^2 成正比,由于在大O表示法中,常数忽略不计,冒泡排序的时间复杂度为O(N^2)

O(N2)的时间复杂度是一个比较糟糕的结果,尤其在数据量很大的情况下。所以冒泡排序通常不会用于实际应用。

11.4 代码改进


传统的冒泡算法每次排序只确定了最大值,我们可以在每次循环之中进行正反两次冒泡,分别找到最大值和最小值,如此可使排序的轮数减少一半

import java.util.Arrays;

public class BubbleSort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

bubbleSort(arr);

// [1, 4, 5, 3, 2, 6]

// [1, 2, 4, 3, 5, 6]

// [1, 2, 3, 4, 5, 6]

}

//冒泡排序改进

public static void bubbleSort(int[] arr) {

int left = 0;

int right = arr.length - 1;

while (left < right) {

//正向冒泡,确定最大值

for (int i = left; i < right; ++i) {

//如果前一位大于后一位,交换位置

if (arr[i] > arr[i + 1]) {

int temp = arr[i];

arr[i] = arr[i + 1];

arr[i + 1] = temp;

}

}

–right;

//反向冒泡,确定最小值

for (int j = right; j > left; --j) {

//如果前一位大于后一位,交换位置

if (arr[j] < arr[j - 1]) {

int temp = arr[j];

arr[j] = arr[j - 1];

arr[j - 1] = temp;

}

}

++left;

System.out.println(Arrays.toString(arr));

}

}

}

第12章 选择排序(含改进版)

=================================================================================

12.1 选择排序概念


选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种

动图展示

动图1:

在这里插入图片描述

动图2:

在这里插入图片描述

12.2 代码实现


import java.util.Arrays;

public class seletsort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

selectSort(arr);

// [1, 5, 6, 3, 2, 4]

// [1, 2, 6, 3, 5, 4]

// [1, 2, 3, 6, 5, 4]

// [1, 2, 3, 4, 5, 6]

// [1, 2, 3, 4, 5, 6]

// [1, 2, 3, 4, 5, 6]

}

//选择排序

public static void selectSort(int[] arr) {

//遍历所有的数

for (int i = 0; i < arr.length; i++) {

int minIndex = i;

//把当前遍历的数和后面所有的数依次进行比较,并记录最小的数的下标

for (int j = i + 1; j < arr.length; j++) {

//如果后面比较的数比记录的最小的数小

if (arr[minIndex] > arr[j]) {

//记录最小的那个数的下标

minIndex = j;

}

}

//如果发现了更小的元素,与第一个元素交换位置(第一个不是最小的元素)

if (i != minIndex) {

int temp = arr[i];

arr[i] = arr[minIndex];

arr[minIndex] = temp;

}

//打印每次排序后的结果

System.out.println(Arrays.toString(arr));

}

}

}

12.3 时间复杂度


  • 最优时间复杂度:O(n^2)

  • 最坏时间复杂度:O(n^2)

  • 稳定性:不稳定(考虑升序每次选择最大的情况)

选择排序与冒泡排序一样,需要进行N*(N-1)/2次比较,但是只需要N次交换,当N很大时,交换次数的时间影响力更大,所以选择排序的时间复杂度为O(N^2)

虽然选择排序与冒泡排序在时间复杂度属于同一量级,但是毫无疑问选择排序的效率更高,因为它的交换操作次数更少,而且在交换操作比比较操作的时间级大得多时,选择排序的速度是相当快的。

12.4 代码改进


传统的选择排序每次只确定最小值,根据改进冒泡算法的经验,我们可以对排序算法进行如下改进:每趟排序确定两个最值——最大值与最小值,这样就可以将排序趟数缩减一半

改进后代码如下:

import java.util.Arrays;

public class seletsort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

selectSort(arr);

// [1, 5, 4, 3, 2, 6]

// [1, 2, 4, 3, 5, 6]

// [1, 2, 3, 4, 5, 6]

}

//选择排序改进

public static void selectSort(int[] arr) {

int minIndex; // 存储最小元素的小标

int maxIndex; // 存储最大元素的小标

for (int i = 0; i < arr.length / 2; i++) {

minIndex = i;

maxIndex = i;

//每完成一轮排序,就确定了两个最值,下一轮排序时比较范围减少两个元素

for (int j = i + 1; j <= arr.length - 1 - i; j++) {

//如果待排数组中的某个元素比当前元素小,minIndex指向该元素的下标

if (arr[j] < arr[minIndex]) {

minIndex = j;

continue;

}

//如果待排数组中的某个元素比当前元素大,maxIndex指向该元素的下标

else if (arr[j] > arr[maxIndex]) {

maxIndex = j;

}

}

//如果发现了更小的元素,与第一个元素交换位置(第一个不是最小的元素)

if (i != minIndex) {

int temp = arr[i];

arr[i] = arr[minIndex];

arr[minIndex] = temp;

// 原来的第一个元素已经与下标为minIndex的元素交换了位置

// 所以现在arr[minIndex]存放的才是之前第一个元素中的数据

// 如果之前maxIndex指向的是第一个元素,那么需要将maxIndex重新指向arr[minIndex]

if (maxIndex == i) {

maxIndex = minIndex;

}

}

// 如果发现了更大的元素,与最后一个元素交换位置

if (arr.length - 1 - i != maxIndex) {

int temp = arr[arr.length - 1 - i];

arr[arr.length - 1 - i] = arr[maxIndex];

arr[maxIndex] = temp;

}

System.out.println(Arrays.toString(arr));

}

}

}

第13章 插入排序(含改进版)

=================================================================================

13.1 插入排序概念


插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间

动图展示

在这里插入图片描述

13.2 代码实现


import java.util.Arrays;

public class InsertSort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

insertSort(arr);

// [4, 5, 6, 3, 2, 1]

// [4, 5, 6, 3, 2, 1]

// [3, 4, 5, 6, 2, 1]

// [2, 3, 4, 5, 6, 1]

// [1, 2, 3, 4, 5, 6]

}

//插入排序

public static void insertSort(int[] arr) {

//遍历所有的数字,从第二个开始和前一个比较

for (int i = 1; i < arr.length; i++) {

//如果当前数字比前一个数字小

if (arr[i] < arr[i - 1]) {

//把当前遍历的数字存起来

int temp = arr[i];

//遍历当前数字前面的数字

int j;

for (j = i - 1; j >= 0 && temp < arr[j]; j–) {

//把前一个数赋给后一个数

arr[j + 1] = arr[j];

}

//把临时变量(外层for循环的当前元素)赋给不满足条件的后一个元素

arr[j + 1] = temp;

}

//打印每次排序后的结果

System.out.println(Arrays.toString(arr));

}

}

}

13.3 时间复杂度


  • 最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)

  • 最坏时间复杂度:O(n^2)

  • 稳定性:稳定

在第一趟排序中,插入排序最多比较一次,第二趟最多比较两次,依次类推,最后一趟最多比较N-1次。因此有:1+2+3+...+N-1 = N*N(N-1)/2

因为在每趟排序发现插入点之前,平均来说,只有全体数据项的一半进行比较,我们除以2得到:N*N(N-1)/4

复制的次数大致等于比较的次数,然而,一次复制与一次比较的时间消耗不同,所以相对于随机数据,这个算法比冒泡排序快一倍,比选择排序略快。

与冒泡排序、选择排序一样,插入排序的时间复杂度仍然为O(N^2),这三者被称为简单排序或者基本排序,三者都是稳定的排序算法。

如果待排序数组基本有序时,插入排序的效率会更高

13.4 代码改进


在插入某个元素之前需要先确定该元素在有序数组中的位置,上例的做法是对有序数组中的元素逐个扫描,当数据量比较大的时候,这是一个很耗时间的过程,可以采用二分查找法改进,这种排序也被称为二分插入排序

import java.util.Arrays;

public class InsertSort {

public static void main(String[] args) {

int[] arr = {4, 5, 6, 3, 2, 1};

insertSort(arr);

// [4, 5, 6, 3, 2, 1]

// [4, 5, 6, 3, 2, 1]

// [3, 4, 5, 6, 2, 1]

// [2, 3, 4, 5, 6, 1]

// [1, 2, 3, 4, 5, 6]

}

//二分插入排序

public static void insertSort(int[] arr) {

for (int i = 1; i < arr.length; i++) {

//如果新记录小于有序序列的最大元素,则用二分法找出新纪录在有序序列中的位置

if (arr[i] < arr[i - 1]) {

int temp = arr[i]; //定义temp存储所要插入的数

int left = 0; //最左边的数,从str[0]开始

int right = i - 1; //最右边位,所要插入那个数的前一位

while (left <= right) {

int middle = (left + right) / 2; //mid中间位

//如果值比中间值大,让left右移到中间下标+1

if (arr[middle] < temp) {

left = middle + 1;

}

//如果值比中间值小,让right左移到中间下标-1

else {

right = middle - 1;

}

}

//以左下标为标准,在左位置前插入该数据,左及左后边全部后移

for (int j = i; j > left; j–) {

arr[j] = arr[j - 1];

}

arr[left] = temp;

}

System.out.println(Arrays.toString(arr));

}

}

}

第14章 归并排序

===========================================================================

14.1 归并排序概念


归并排序(Merge Sort)是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

动图展示

  • 动图1

在这里插入图片描述

  • 动图2

在这里插入图片描述

14.2 代码实现


import java.util.Arrays;

public class MergeSort {

public static void main(String[] args) {

int[] arr = {6, 5, 3, 1, 8, 7, 2, 4};

mergeSort(arr, 0, arr.length - 1);

// [5, 6, 3, 1, 8, 7, 2, 4]

// [5, 6, 1, 3, 8, 7, 2, 4]

// [1, 3, 5, 6, 8, 7, 2, 4]

// [1, 3, 5, 6, 7, 8, 2, 4]

// [1, 3, 5, 6, 7, 8, 2, 4]

// [1, 3, 5, 6, 2, 4, 7, 8]

// [1, 2, 3, 4, 5, 6, 7, 8]

}

//归并排序

public static void mergeSort(int[] arr, int low, int high) {

int middle = (high + low) / 2;

//递归结束

if (low < high) {

//处理左边

mergeSort(arr, low, middle);

//处理右边

mergeSort(arr, middle + 1, high);

//归并

merge(arr, low, middle, high);

}

}

//归并操作

//low:开始位置,middle:分割位置,high:结束位置

public static void merge(int[] arr, int low, int middle, int high) {

//用于存储归并后的临时数组

int[] temp = new int[high - low + 1];

//记录第一个数组中需要遍历的下标

int i = low;

//记录第二个数组中需要遍历的下标

int j = middle + 1;

//用于记录在临时数组中存放的下标

int index = 0;

//遍历两个数组取出小的数字,放入临时数组中

while (i <= middle && j <= high) {

//第一个数组的数据更小

if (arr[i] <= arr[j]) {

//把小的数组放入临时数组中

temp[index] = arr[i];

//让下标向后移一位

i++;

} else {

temp[index] = arr[j];

j++;

}

//每存入一个数字后,临时数组下标后移

index++;

}

//上面的循环退出后,把剩余的元素依次填入到temp中,以下两个while只有一个会执行

//前面一个数组有多余数据

while (i <= middle) {

temp[index] = arr[i];

i++;

index++;

}

//后面一个数组有多余数据

while (j <= high) {

temp[index] = arr[j];

j++;

index++;

}

//把临时数组中的数据重新存入原数组

for (int k = 0; k < temp.length; k++) {

arr[k + low] = temp[k];

}

//打印每次排序后的结果

System.out.println(Arrays.toString(arr));

}

}

14.3 时间复杂度


最优时间复杂度:O(nlogn)

最坏时间复杂度:O(nlogn)

稳定性:稳定

第15章 快速排序

===========================================================================

15.1 快速排序概念


快速排序(Quick Sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

排序步骤

  • 1、 从数列中挑出一个元素,称为"基准"(pivot),通常选择第一个元素

  • 2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

  • 3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

动图展示

  • 动图1

在这里插入图片描述

  • 动图2:

在这里插入图片描述

静图分析

在这里插入图片描述

15.2 代码实现


import java.util.Arrays;

public class QuickSort {

public static void main(String[] args) {

int[] arr = {30, 40, 60, 10, 20, 50};

quickSort(arr, 0, arr.length - 1);

// [20, 10, 30, 60, 40, 50]

// [10, 20, 30, 60, 40, 50]

// [10, 20, 30, 60, 40, 50]

// [10, 20, 30, 50, 40, 60]

// [10, 20, 30, 40, 50, 60]

// [10, 20, 30, 40, 50, 60]

}

//快速排序

public static void quickSort(int[] arr, int start, int end) {

//递归结束的标记

if (start < end) {

//把数组中第0个数字作为标准数

int stard = arr[start];

//记录需要排序的下标

int low = start;

int high = end;

//循环找比标准数大的数和标准数小的数

while (low < high) {

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!
//二分插入排序

public static void insertSort(int[] arr) {

for (int i = 1; i < arr.length; i++) {

//如果新记录小于有序序列的最大元素,则用二分法找出新纪录在有序序列中的位置

if (arr[i] < arr[i - 1]) {

int temp = arr[i]; //定义temp存储所要插入的数

int left = 0; //最左边的数,从str[0]开始

int right = i - 1; //最右边位,所要插入那个数的前一位

while (left <= right) {

int middle = (left + right) / 2; //mid中间位

//如果值比中间值大,让left右移到中间下标+1

if (arr[middle] < temp) {

left = middle + 1;

}

//如果值比中间值小,让right左移到中间下标-1

else {

right = middle - 1;

}

}

//以左下标为标准,在左位置前插入该数据,左及左后边全部后移

for (int j = i; j > left; j–) {

arr[j] = arr[j - 1];

}

arr[left] = temp;

}

System.out.println(Arrays.toString(arr));

}

}

}

第14章 归并排序

===========================================================================

14.1 归并排序概念


归并排序(Merge Sort)是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。

将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。

动图展示

  • 动图1

在这里插入图片描述

  • 动图2

在这里插入图片描述

14.2 代码实现


import java.util.Arrays;

public class MergeSort {

public static void main(String[] args) {

int[] arr = {6, 5, 3, 1, 8, 7, 2, 4};

mergeSort(arr, 0, arr.length - 1);

// [5, 6, 3, 1, 8, 7, 2, 4]

// [5, 6, 1, 3, 8, 7, 2, 4]

// [1, 3, 5, 6, 8, 7, 2, 4]

// [1, 3, 5, 6, 7, 8, 2, 4]

// [1, 3, 5, 6, 7, 8, 2, 4]

// [1, 3, 5, 6, 2, 4, 7, 8]

// [1, 2, 3, 4, 5, 6, 7, 8]

}

//归并排序

public static void mergeSort(int[] arr, int low, int high) {

int middle = (high + low) / 2;

//递归结束

if (low < high) {

//处理左边

mergeSort(arr, low, middle);

//处理右边

mergeSort(arr, middle + 1, high);

//归并

merge(arr, low, middle, high);

}

}

//归并操作

//low:开始位置,middle:分割位置,high:结束位置

public static void merge(int[] arr, int low, int middle, int high) {

//用于存储归并后的临时数组

int[] temp = new int[high - low + 1];

//记录第一个数组中需要遍历的下标

int i = low;

//记录第二个数组中需要遍历的下标

int j = middle + 1;

//用于记录在临时数组中存放的下标

int index = 0;

//遍历两个数组取出小的数字,放入临时数组中

while (i <= middle && j <= high) {

//第一个数组的数据更小

if (arr[i] <= arr[j]) {

//把小的数组放入临时数组中

temp[index] = arr[i];

//让下标向后移一位

i++;

} else {

temp[index] = arr[j];

j++;

}

//每存入一个数字后,临时数组下标后移

index++;

}

//上面的循环退出后,把剩余的元素依次填入到temp中,以下两个while只有一个会执行

//前面一个数组有多余数据

while (i <= middle) {

temp[index] = arr[i];

i++;

index++;

}

//后面一个数组有多余数据

while (j <= high) {

temp[index] = arr[j];

j++;

index++;

}

//把临时数组中的数据重新存入原数组

for (int k = 0; k < temp.length; k++) {

arr[k + low] = temp[k];

}

//打印每次排序后的结果

System.out.println(Arrays.toString(arr));

}

}

14.3 时间复杂度


最优时间复杂度:O(nlogn)

最坏时间复杂度:O(nlogn)

稳定性:稳定

第15章 快速排序

===========================================================================

15.1 快速排序概念


快速排序(Quick Sort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

排序步骤

  • 1、 从数列中挑出一个元素,称为"基准"(pivot),通常选择第一个元素

  • 2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

  • 3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

动图展示

  • 动图1

在这里插入图片描述

  • 动图2:

在这里插入图片描述

静图分析

在这里插入图片描述

15.2 代码实现


import java.util.Arrays;

public class QuickSort {

public static void main(String[] args) {

int[] arr = {30, 40, 60, 10, 20, 50};

quickSort(arr, 0, arr.length - 1);

// [20, 10, 30, 60, 40, 50]

// [10, 20, 30, 60, 40, 50]

// [10, 20, 30, 60, 40, 50]

// [10, 20, 30, 50, 40, 60]

// [10, 20, 30, 40, 50, 60]

// [10, 20, 30, 40, 50, 60]

}

//快速排序

public static void quickSort(int[] arr, int start, int end) {

//递归结束的标记

if (start < end) {

//把数组中第0个数字作为标准数

int stard = arr[start];

//记录需要排序的下标

int low = start;

int high = end;

//循环找比标准数大的数和标准数小的数

while (low < high) {

最后

无论是哪家公司,都很重视基础,大厂更加重视技术的深度和广度,面试是一个双向选择的过程,不要抱着畏惧的心态去面试,不利于自己的发挥。同时看中的应该不止薪资,还要看你是不是真的喜欢这家公司,是不是能真的得到锻炼。

针对以上面试技术点,我在这里也做一些分享,希望能更好的帮助到大家。

[外链图片转存中…(img-T3Uilmrv-1714774948601)]

[外链图片转存中…(img-4BQswMlW-1714774948601)]

[外链图片转存中…(img-e9AOeHm2-1714774948601)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门,即可获取!

  • 13
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值