可以扫描下面二维码访问我的小程序来打开,随时随地通过微信访问。
这部分要会手动实现一些数据结构,我总结了以下一些重要的数据结构
数据结构
链表(增删查操作)
1.链表-单向链表
/**
* 单向链表
*/
public class SinglyLinkedList {
private SinglyLinkedNode head;
public static class SinglyLinkedNode {
public SinglyLinkedNode (String value) {
this.value = value;
}
private SinglyLinkedNode next;
private String value;
public SinglyLinkedNode getNext() {
return next;
}
public void setNext(SinglyLinkedNode next) {
this.next = next;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "SinglyLinkedNode{" +
"value='" + value + '\'' +
'}';
}
}
public void add(String value){
if (head == null) {
head = new SinglyLinkedNode(value);
} else {
SinglyLinkedNode next = head.getNext();
SinglyLinkedNode pre = null;
if(next==null){
head.setNext(new SinglyLinkedNode(value));
return;
}
while(next!=null) {
pre = next;
next = next.getNext();
}
pre.setNext(new SinglyLinkedNode(value));
}
}
public SinglyLinkedNode search(String value) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
return head;
} else {
SinglyLinkedNode next = head.getNext();
while(next!=null){
if(value==next.getValue()){
return next;
}
next = next.getNext();
}
return null;
}
}
}
public void remove(String value) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
head = head.getNext();
} else {
SinglyLinkedNode pre = head;
SinglyLinkedNode next = head.getNext();
while(next!=null&&next.getValue()!=value){
pre = next;
next = next.getNext();
}
//找到了
if(next!=null&&value==next.value){
pre.setNext(next.getNext());
}
}
}
}
public void set(String value, String newValue) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
SinglyLinkedNode newHead = new SinglyLinkedNode(newValue);
newHead.setNext(head.getNext());
head = newHead;
} else {
SinglyLinkedNode pre = head;
SinglyLinkedNode next = head.getNext();
while(next!=null){
if(value==next.getValue()){
SinglyLinkedNode newNode = new SinglyLinkedNode(newValue);
newNode.setNext(next.getNext());
pre.setNext(newNode);
break;
}
next = next.getNext();
}
}
}
}
public static void main(String[] args) throws Exception{
SinglyLinkedList link = new SinglyLinkedList();
link.add("a");
link.add("b");
link.add("c");
System.out.println(link);
System.out.println(link.search("b"));
System.out.println(link.search("d"));
link.remove("b");
System.out.println(link);
link.set("c","d");
System.out.println(link);
}
@Override
public String toString() {
String output = "";
SinglyLinkedNode cur = head;
while(cur != null){
output += "["+cur+"],";
cur = cur.getNext();
}
return "SinglyLinkedList{"
+ output +
'}';
}
}
//输出
SinglyLinkedList{[SinglyLinkedNode{value='a'}],[SinglyLinkedNode{value='b'}],[SinglyLinkedNode{value='c'}],}
SinglyLinkedNode{value='b'}
null
SinglyLinkedList{[SinglyLinkedNode{value='a'}],[SinglyLinkedNode{value='c'}],}
SinglyLinkedList{[SinglyLinkedNode{value='a'}],[SinglyLinkedNode{value='d'}],}
2.链表-双向链表
public class DoublyLinkedList {
private DoublyLinkedNode head;
public void add(String value){
if (head == null) {
head = new DoublyLinkedNode(value);
} else {
DoublyLinkedNode next = head.getNext();
DoublyLinkedNode pre = null;
if(next==null){
DoublyLinkedNode newNode = new DoublyLinkedNode(value);
newNode.setPre(head);
head.setNext(newNode);
return;
}
while(next!=null) {
pre = next;
next = next.getNext();
}
DoublyLinkedNode newNode = new DoublyLinkedNode(value);
newNode.setPre(pre);
pre.setNext(newNode);
}
}
public DoublyLinkedNode search(String value) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
return head;
} else {
DoublyLinkedNode next = head.getNext();
while(next!=null){
if(value==next.getValue()){
return next;
}
next = next.getNext();
}
return null;
}
}
}
public void remove(String value) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
DoublyLinkedNode next = head.getNext();
next.setPre(null);
head = next;
} else {
DoublyLinkedNode pre = head;
DoublyLinkedNode next = head.getNext();
while(next!=null&&next.getValue()!=value){
pre = next;
next = next.getNext();
}
//找到了
if(next!=null && value==next.value){
DoublyLinkedNode newNext = next.getNext();
pre.setNext(newNext);
if(newNext!=null) {
newNext.setPre(pre);
}
next.setNext(null);
next.setPre(null);
}
}
}
}
public void set(String value, String newValue) throws Exception{
if (head == null) {
throw new Exception();
} else {
if (head.getValue() == value) {
DoublyLinkedNode newHead = new DoublyLinkedNode(newValue);
newHead.setNext(head.getNext());
head.next = null;
head = newHead;
} else {
DoublyLinkedNode pre = head;
DoublyLinkedNode next = head.getNext();
while(next!=null){
if(value==next.getValue()){
DoublyLinkedNode newNode = new DoublyLinkedNode(newValue);
newNode.setNext(next.getNext());
newNode.setPre(next.getPre());
pre.setNext(newNode);
if(next.getNext()!=null) {
next.getNext().setPre(newNode);
}
next.setPre(null);
next.setNext(null);
break;
}
pre = next;
next = next.getNext();
}
}
}
}
public static void main(String[] args) throws Exception{
DoublyLinkedList link = new DoublyLinkedList();
link.add("a");
link.add("b");
link.add("c");
link.add("d");
link.add("e");
System.out.println(link);
System.out.println(link.search("c"));
link.remove("e");
System.out.println(link);
link.set("d","f");
System.out.println(link);
}
public static class DoublyLinkedNode {
public DoublyLinkedNode(String value) {
this.value = value;
}
private DoublyLinkedNode pre;
private DoublyLinkedNode next;
private String value;
public DoublyLinkedNode getPre() {
return pre;
}
public void setPre(DoublyLinkedNode pre) {
this.pre = pre;
}
public DoublyLinkedNode getNext() {
return next;
}
public void setNext(DoublyLinkedNode next) {
this.next = next;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "DoublyLinkedNode{" +
"value='" + value + '\'' +
'}';
}
}
@Override
public String toString() {
String output = "";
DoublyLinkedNode cur = head;
while(cur != null){
output += "["+cur+"],";
cur = cur.getNext();
}
return "DoublyLinkedList{"
+ output +
'}';
}
}
//输出
DoublyLinkedList{[DoublyLinkedNode{value='a'}],[DoublyLinkedNode{value='b'}],[DoublyLinkedNode{value='c'}],[DoublyLinkedNode{value='d'}],[DoublyLinkedNode{value='e'}],}
DoublyLinkedNode{value='c'}
DoublyLinkedList{[DoublyLinkedNode{value='a'}],[DoublyLinkedNode{value='b'}],[DoublyLinkedNode{value='c'}],[DoublyLinkedNode{value='d'}],}
DoublyLinkedList{[DoublyLinkedNode{value='a'}],[DoublyLinkedNode{value='b'}],[DoublyLinkedNode{value='c'}],[DoublyLinkedNode{value='f'}],}
队列(增删查操作)
3.队列-普通队列单链表结构
package com.xuesong.datastructure.queue;
/**
* 先进先出普通队列
* 通过单链表实现,队列内元素个数不需要初始化,队列内元素个数可以放入很多
* @param <E>
*/
public class NormalQueue<E> {
private SinglyLinkedNode first;
private SinglyLinkedNode last;
/**
* 通过单链表即可实现
* @param <E>
*/
public static class SinglyLinkedNode <E> {
public SinglyLinkedNode (E value) {
this.value = value;
}
private E value;
private SinglyLinkedNode next;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
@Override
public String toString() {
return "SinglyLinkedNode{" +
"value=" + value +
'}';
}
}
/**
* 从队尾插入新节点
* @param e
*/
public void push(E e){
SinglyLinkedNode newNode = new SinglyLinkedNode(e);
if (first == null) {
first = newNode;
}
if (last == null) {
last = newNode;
} else {
last.next = newNode;
last = newNode;
}
}
/**
* 从队首取出节点
*/
public E pop(){
if (first == null) {
return null;
}
SinglyLinkedNode<E> node = first;
first = first.next;
if(first == null){
last = null;
}
return node.getValue();
}
public static void main(String[] args) {
NormalQueue<String> queue = new NormalQueue<String>();
queue.push("a");
queue.push("b");
queue.push("c");
queue.push("d");
String result = queue.pop();
while(result!=null){
System.out.println("pop == "+result);
result = queue.pop();
}
}
}
//输出
pop == a
pop == b
pop == c
pop == d
4.队列-普通队列数组结构
package com.xuesong.datastructure.queue;
/**
* 先进先出普通队列
* 通过数组实现,队列内的元素个数需要初始化,有限制
* @param
*/
public class NoramlArrayQueue {
private Object[] arr;
private int maxSize = -1;
private int curIndex = -1;
public NoramlArrayQueue (int size){
arr = new Object[size];
maxSize = size;
}
/**
* 从队尾插入新节点
* @param
*/
public void push(Object e) throws Exception {
if (curIndex >= maxSize-1) {
throw new Exception("Queue if full");
}
arr[++curIndex] = e;
}
public Object pop() throws Exception {
if(curIndex == -1){
return null;
}
return arr[curIndex--];
}
public static void main(String[] args) throws Exception{
NoramlArrayQueue queue = new NoramlArrayQueue(10);
queue.push("a");
queue.push("b");
queue.push("c");
queue.push("d");
queue.push("e");
queue.push("f");
queue.push("g");
queue.push("h");
queue.push("i");
queue.push("j");
//到达这个放入k就会报满了的异常
// queue.push("k");
String result = (String)queue.pop();
while(result!=null){
System.out.println("pop == "+result);
result = (String)queue.pop();
}
//由于之前已经清空了,重复放入新值即可
queue.push("1");
queue.push("2");
queue.push("3");
queue.push("4");
queue.push("5");
queue.push("6");
queue.push("7");
queue.push("8");
queue.push("9");
result = (String)queue.pop();
while(result!=null){
System.out.println("pop == "+result);
result = (String)queue.pop();
}
}
}
//输出
pop == j
pop == i
pop == h
pop == g
pop == f
pop == e
pop == d
pop == c
pop == b
pop == a
pop == 9
pop == 8
pop == 7
pop == 6
pop == 5
pop == 4
pop == 3
pop == 2
pop == 1
树
5.树-二叉树(前序、中序、后序)
package com.xuesong.datastructure.tree;
/**
* 二叉树
*
*/
public class BinaryTree<E> {
private TreeNode head;
public BinaryTree () {
}
public static class TreeNode <E> {
public TreeNode (E value) {
this.value = value;
}
private E value;
private TreeNode left;
private TreeNode right;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
@Override
public String toString() {
return "TreeNode{" +
"value=" + value +
'}';
}
}
/**
* 前序输出,先根节点,再左节点,最后右节点
*/
public void prePrint(TreeNode node) {
if(node==null){
return;
}
System.out.print(node.value+"-");
prePrint(node.left);
prePrint(node.right);
}
/**
* 中序输出,先左,后根,最后右
*/
public void midPrint(TreeNode node) {
if(node==null){
return;
}
midPrint(node.left);
System.out.print(node.value+"-");
midPrint(node.right);
}
/**
* 后序输出,先左后右最后根
*/
public void lastPrint(TreeNode node) {
if(node==null){
return;
}
lastPrint(node.left);
lastPrint(node.right);
System.out.print(node.value+"-");
}
/** 1
* / \
* 2 3
* / / \
* 4 5 6
* / \ /
* 7 8 9
* 树的结构
*/
public static void main(String[] args) {
TreeNode<String> node1 = new TreeNode<String>("1");
TreeNode<String> node2 = new TreeNode<String>("2");
TreeNode<String> node3 = new TreeNode<String>("3");
TreeNode<String> node4 = new TreeNode<String>("4");
TreeNode<String> node5 = new TreeNode<String>("5");
TreeNode<String> node6 = new TreeNode<String>("6");
TreeNode<String> node7 = new TreeNode<String>("7");
TreeNode<String> node8 = new TreeNode<String>("8");
TreeNode<String> node9 = new TreeNode<String>("9");
node1.setLeft(node2);
node1.setRight(node3);
node2.setLeft(node4);
node3.setLeft(node5);
node3.setRight(node6);
node4.setLeft(node7);
node4.setRight(node8);
node5.setLeft(node9);
BinaryTree binaryTree = new BinaryTree();
//前序
System.out.println("前序");
binaryTree.prePrint(node1);
//中序
System.out.println("");
System.out.println("中序");
binaryTree.midPrint(node1);
//后续
System.out.println("");
System.out.println("后序");
binaryTree.lastPrint(node1);
}
}
//输出
前序
1-2-4-7-8-3-5-9-6-
中序
7-4-8-2-1-9-5-3-6-
后序
7-8-4-2-9-5-6-3-1-
6. 树-二叉排序树
二叉排序树,任意结点左子树的所有结点的值都小于该节点的值且该结点右子树的值都大于该节点的值。
以下两个图都是二叉排序树
7.树-平衡树(尽量会实现代码)
(1)二叉排序树的定义,某结点左子树的所有结点的值都小于该节点的值且该结点右子树的值都大于该节点的值。
(2)平衡二叉树是特殊的二叉排序树。
(4)每个节点都有一个平衡因子,也就是该节点的左子树高度-右子树高度。
(4)平衡二叉树必须满足以下两个条件:必须是二叉排序树;平衡因子的绝对值小于等于1。
(5)代码实现有些复杂,写一下基本逻辑吧,其实只有四种情况
(6)LL型平衡旋转法,由于在A的左孩子B的左子树上插入结点F,使A的平衡因子由1增至2而失去平衡。例如下图,新增加了2节点,导致失去平衡。分别标出了各个节点的负载因子,其中5节点,负载因子是2(左子树高度2,右子树高度0),3节点,负载因子1(左子树高度1,右子树高度0),直接右旋(向右旋转),3变成跟节点,2和5变为叶子结点。
(7)RR型平衡旋转法,由于在A的右孩子C 的右子树上插入结点F,使A的平衡因子由-1减至-2而失去平衡。例如下图,新增加了5节点,导致失去平衡。分别标出了各个节点的负载因子,其中2节点,负载因子是-2(左子树高度0,右子树高度2),3节点,负载因子-1(左子树高度0,右子树高度1),直接左旋(向左旋转),3变成跟节点,2和5变为叶子结点。
(8)LR型平衡旋转法,由于在A的左孩子B的右子数上插入结点F,使A的平衡因子由1增至2而失去平衡。例如下图,新增了4节点,导致失去平衡。分别标出了各个节点的负载因子,其中5节点,负载因子是2(左子树高度2,右子树高度0),3节点,负载因子-1(左子树高度0,右子树高度1),这个调整略复杂,需进行两次旋转操作(先右旋,后左旋)。先将3节点和4节点右旋,变成LL型,在按照LL型右旋即可。
(9)RL型平衡旋转法,由于在A的右孩子C的左子树上插入结点F,使A的平衡因子由-1减至-2而失去平衡。例如下图,新增了4节点,导致失去平衡。分别标出了各个节点的负载因子,其中3节点,负载因子是-2(左子树高度0,右子树高度2),5节点,负载因子1(左子树高度1,右子树高度0),这个调整略复杂,需进行两次旋转操作(先左旋,后右旋)。先将4节点和5节点左旋,变成RR型,在按照RR型左旋即可。
8.树-红黑树(了解性质、应用场景)
(1)为什么会产生红黑树呢?从二叉排序开始,二叉排序树的性质会导致他出现极端偏向一方的情况,不平衡,因此产生了二叉平衡树,这种树分布十分平均,查询效率非常高,但是几乎每一次插入都会导致再平衡,因此根据二叉平衡树的一些性质,修改了二叉平衡树的一些性质,增加了红黑节点,造了一棵不严格平衡的二叉平衡树,也就是上面说的,大体上平衡的红黑树。不平衡在哪里呢?
(2)红黑树不平衡在哪里呢?新增加的节点默认是红色,主要是因为插入黑色可能节点会影响黑色高度,对红黑树的影响更大,而红色的节点在红黑树中不计算高度,也就是说,去掉所有红节点后,剩余的黑节点就是一个二叉平衡树,所以默认新增都是红色节点。那么如果算上红节点,整棵树就不是一个二叉平衡树。接下来看下性质
(3)节点不是黑色,就是红色(非黑即红)
(4)根节点为黑色
(5)叶节点为黑色
(6)一个节点为红色,则其两个子节点必须是黑色的(根到叶子的所有路径,不可能存在两个连续的红色节点)
(7)每个节点到叶子节点的所有路径,都包含相同数目的黑色节点(相同的黑色高度)
(8)以上都是红黑树的性质,其实咱们需要了解红黑树实际上是一棵不严格的平衡二叉树,大体上符合平衡二叉树,但是通过红黑节点变色的性质后,可以不严格满足平衡因子绝对值小于等于1的性质
(9)这些性质十分繁琐,可以先背过来。
9.树-B树(了解性质、应用场景)
(1)为什么会产生B树呢?由于红黑树,和平衡二叉树,整体存储的数量不多,对于像数据库这种,存储大量数据,几十万几百万甚至上千万数据的情况,使用二叉树,效率也是极低的。这种结构会造成当数据量非常大时,二叉查找树的高度非常大,搜索算法从根节点向下搜索时,需要访问的节点数会变多,如果这些节点信息存储在外存储器(磁盘)中,每访问一个节点,就相当于进行了一次I/O操作,而频繁的I/O操作会降低查询的效率。要想提高效率,就要减少磁盘I/O的次数,所以我们要在磁盘页面上多存储一些信息,所以B树就是扩展每个节点的信息,降低树的高度,增加了效率。
(2)B树的阶,节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。
(3)在B树上,关键字集合分布在整棵树中,即叶子节点和非叶子节点都存放数据。搜索有可能在非叶子节点结束。
(4)一棵m阶B树每个节点至多有m棵子树
(5)除根节点外,其他分支节点至少有𝑐𝑒𝑖𝑙(𝑚/2)棵子树;根节点至少有两棵子树(除非B树只包含一个节点),例如5阶B树,跟节点可以有2个节点,但是下面的节点,最少有5/2=2.5,向上取整为3,所以其他节点至少有3个子节点
(6)有𝑗个孩子节点的非叶节点有𝑗−1个关键字,关键字按非降序排列,关键字实际就是存入的值
(7)所有叶子节点具有相同的深度,这也说明B树是平衡的,B-tree的名字也是这样来的
(8)在搜索B树时,访问节点(即读取磁盘)的次数与树的高度呈正比,而B树与二叉查找树相比,虽然高度都是对数级的,但是显然B树中底数比2大。因此,和二叉树相比,极大地减少了磁盘读取的次数。说人话就是,每次向下一层查找都要读取一次磁盘,而B树的层级明显比平衡二叉树要少的,因此向下查找,查询磁盘的次数肯定要笑。
图论
10.图论-深度优先搜索
(1)深度优先搜索(DFS),是一种在图、搜索树和遍历中非常经典且常用的算法。其思路为用递归的方式首先延一条路线搜索到底,如果到底了还没搜索到对象,就回溯到上一层并按另一条路线再走到底,如此重复直到找到对象或者遍历完整个集合为止。
(2)具体搜索路径可以如下图:
(3)实际也就是我写的树的前序中序后序遍历算法实际就是深度优先搜索。
package com.xuesong.datastructure.tree;
/**
* 二叉树
*
*/
public class BinaryTree<E> {
private TreeNode head;
public BinaryTree () {
}
public static class TreeNode <E> {
public TreeNode (E value) {
this.value = value;
}
private E value;
private TreeNode left;
private TreeNode right;
public E getValue() {
return value;
}
public void setValue(E value) {
this.value = value;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
@Override
public String toString() {
return "TreeNode{" +
"value=" + value +
'}';
}
}
/**
* 前序输出,先根节点,再左节点,最后右节点
*/
public void prePrint(TreeNode node) {
if(node==null){
return;
}
System.out.print(node.value+"-");
prePrint(node.left);
prePrint(node.right);
}
/**
* 中序输出,先左,后根,最后右
*/
public void midPrint(TreeNode node) {
if(node==null){
return;
}
midPrint(node.left);
System.out.print(node.value+"-");
midPrint(node.right);
}
/**
* 后序输出,先左后右最后根
*/
public void lastPrint(TreeNode node) {
if(node==null){
return;
}
lastPrint(node.left);
lastPrint(node.right);
System.out.print(node.value+"-");
}
/** 1
* / \
* 2 3
* / / \
* 4 5 6
* / \ /
* 7 8 9
* 树的结构
*/
public static void main(String[] args) {
TreeNode<String> node1 = new TreeNode<String>("1");
TreeNode<String> node2 = new TreeNode<String>("2");
TreeNode<String> node3 = new TreeNode<String>("3");
TreeNode<String> node4 = new TreeNode<String>("4");
TreeNode<String> node5 = new TreeNode<String>("5");
TreeNode<String> node6 = new TreeNode<String>("6");
TreeNode<String> node7 = new TreeNode<String>("7");
TreeNode<String> node8 = new TreeNode<String>("8");
TreeNode<String> node9 = new TreeNode<String>("9");
node1.setLeft(node2);
node1.setRight(node3);
node2.setLeft(node4);
node3.setLeft(node5);
node3.setRight(node6);
node4.setLeft(node7);
node4.setRight(node8);
node5.setLeft(node9);
BinaryTree binaryTree = new BinaryTree();
//前序
System.out.println("前序");
binaryTree.prePrint(node1);
//中序
System.out.println("");
System.out.println("中序");
binaryTree.midPrint(node1);
//后续
System.out.println("");
System.out.println("后序");
binaryTree.lastPrint(node1);
}
}
//输出
前序
1-2-4-7-8-3-5-9-6-
中序
7-4-8-2-1-9-5-3-6-
后序
7-8-4-2-9-5-6-3-1-
11.图论-广度优先搜索
(1)广度优先搜索BFS,同深度优先搜索DFS一样,是一种用于遍历、搜索树或图的一种搜索算法。与DFS会先一路走到黑不同,BFS会从根节点开始搜索,在每一个路口面临分叉的时候,先把每个岔路记录下来,然后再去一个一个的往前走一步。
(2)具体搜索路径可以如下图:
(3)具体逻辑,例如上图,先把根节点0存入队列(先进先出队列),然后从队列中取出第一个节点,查找这个节点也就是0节点的所有相连接的节点,也就是节点1和节点2,将这两个节点放入队列,之后取出下个节点,也就是节点1,查找这个节点的所有相连接的节点,也就是节点3和节点4,放入队列,实际还有节点0也与节点1连接,但是由于之前已经遍历过了,不再存入队列,取出队列中的下个节点,也就是节点2,查找这个节点所有相连接的节点,也就是节点5和节点6,放入队列,之后从队列中取出下个节点,节点4,没有连接节点,再从队列中取出下个节点,节点4,没有连接节点,再次从队列中取出下个节点5,没有连接,再次从队列中取出下个节点6,没有连接,至此队列中没有节点,搜索完毕。
(4)其中为了避免重复节点存入队列,需要单独设置一个hashSet,遍历过程中,判断是否再hashSet中存在,如果存在就不放入队列中了,不存在就分别放入队列和hashSet中。
12.图论-最短路径-FLoyd弗洛伊德算法
(1)目的是解决多源最短路径。
(2)这是一种数学的邻接矩阵的算法,也不复杂,先看下图
(3)可以看出每个节点到相邻节点的距离,例如A到B,B到A的距离都是12,C到D,D到C都是3,自己到自己是0。因此可以直接写出邻接矩阵,第一步只写出相邻节点的距离,不相邻的先写为*,表示未知,同时需要存储目前相连节点的路径,定义二位数组,数组汇总的实体内容为距离和当前路径
例如A-B,B-A都是12,路径就分别是A-B和B-A,A-B的数组就是a[0][1]=距离12,路径A-B
B-A的数组就是a[1][0]=(距离12,路径B-A)
先写出距离的邻接矩阵,具体如下:
A | B | C | D | E | F | G | |
---|---|---|---|---|---|---|---|
A | 0 | 12 | * | * | * | 16 | 14 |
B | 12 | 0 | 10 | * | * | 7 | * |
C | * | 10 | 0 | 3 | 5 | 6 | * |
D | * | * | 3 | 0 | 4 | * | * |
E | * | * | 5 | 4 | 0 | 2 | 8 |
F | 16 | 7 | 6 | * | 2 | 0 | 9 |
G | 14 | * | * | * | 8 | 9 | 0 |
(4)接下来,把定点A作为中间节点,更新都与A相邻(与A的距离不是的节点)的节点,但是这两个节点不相邻(实际上是中间距离为的节点)的节点
遍历与A相邻(与A的距离不是*的节点)的节点,分别是BFG
遍历到B时,再次遍历B节点中与B距离为的节点,分别是DEG,查看DEG中是否存在与A节点有距离的节点,可以找到是G节点与A节点存在距离,那么就可以知道B节点与G节点的距离就是B-A-G节点的距离,也就是12+14=26,修改该节点a[1][6]=(距离26,路径B-A-G),遍历B中与B距离为的节点完毕后,跳出遍历
遍历到F时,再次遍历F节点中与F距离为*的节点,只有D,查看D是否存在与A节点有距离的节点,没有,跳出遍历
遍历到G时,再次遍历G节点中与G距离为的节点,分别是BCD,查看BCD中是否存在与A节点有距离的节点,可以找到是B节点与A节点存在距离,那么就可以知道G节点与B节点的距离就是G-A-B节点的距离,也就是14+12=26,修改该节点a[6][1]=(距离26,路径G-A-B),遍历G中与G距离为的节点完毕后,跳出遍历
可以画出新的邻接矩阵,具体如下
A | B | C | D | E | F | G | |
---|---|---|---|---|---|---|---|
A | 0 | 12 | * | * | * | 16 | 14 |
B | 12 | 0 | 10 | * | * | 7 | 26 |
C | * | 10 | 0 | 3 | 5 | 6 | * |
D | * | * | 3 | 0 | 4 | * | * |
E | * | * | 5 | 4 | 0 | 2 | 8 |
F | 16 | 7 | 6 | * | 2 | 0 | 9 |
G | 14 | 26 | * | * | 8 | 9 | 0 |
(5)接下来,把定点B作为中间节点,更新都与B相邻(与B的距离不是的节点)的节点,但是这两个节点不相邻(实际上是中间距离为的节点)的节点,和(4)种的遍历算法一致,补充距离以及路径。之后再以分别CDEFG遍历,计算出最短路径,最终数组a[][]存储的就是最短路径以及最短距离了。
13.图论-最短路径-Dijkstra迪杰斯特拉算法
(1)目的是解决单源最短路径,也就是指定源点,计算出该点到其他所有节点的最短路径。
(2)采用贪心算法的策略,将所有顶点分为已标记点和未标记点两个集合,从起始点开始,不断在未标记点中寻找距离起始点路径最短的顶点,并将其标记,直到所有顶点都被标记为止。说人话就是在图中先找到距离源点最近的点,并记录该点,然后通过该点作为跳板,补充或者替换其他节点到源点距离,接下来找倒数第二近的点,以该点作为跳板,补充或者替换其他节点(已经被记录的点不再需要替换)到源点的距离,接下来找倒数第三近,第四近的节点来作为跳板,一个个来,直到所有节点被标记。
(3)下面咱们来看一个图列子
(4)咱们先来指定源点为节点0,计算到其他节点的最短距离。
(5)写出矩阵,标识节点0到其他节点距离,其中起点为节点0,终点分别为节点12345,路径指的是到达终点需要达到的前置节点,距离也就是需要经过的距离,初始可以达到写出距离,不可达写为无穷大∞
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1 | 节点0 | 6 |
节点2 | 节点0 | 3 | |
节点3 | 不可达 | ∞ | |
节点4 | 不可达 | ∞ | |
节点5 | 不可达 | ∞ |
(6)循环可以找到距离最小节点为节点2,源点到节点2距离为3(后边计算距离通过节点2到达其他节点需要累加距离3),记录该节点,通过该节点为跳板,找到通过节点2可以到达的节点以及计算距离,与当前存在的距离比较大小,如果小于当前存在距离,替换当前距离为刚刚计算出来的距离。
循环节点2可以到达的所有节点除去源点,分别为节点1,节点3,节点4。
可知通过节点2可以达到节点1,节点2到达节点1距离为2,因此节点0通过节点2到达节点1距离为3(源点到节点2距离)+2(节点2到达节点1距离)=5,是小于当前表中的距离6的,因此更新节点0到节点1的距离为5,路径修改为前置节点2。
可知通过节点2可以达到节点3,节点2到达节点3距离为3,因此节点0通过节点2到达节点3距离为3+3=6,是小于当前表中的距离∞的,因此更新节点0到节点3的距离为6,路径修改不可达为前置节点2。
可知通过节点2可以达到节点4,节点2到达节点4距离为4,因此节点0通过节点2到达节点4距离为3+4=7,是小于当前表中的距离∞的,因此更新节点0到节点3的距离为7,路径修改不可达为前置节点2。
本次修改完毕后,可以看到下面矩阵有所变化
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1 | 节点2(之前为节点0,更新为节点2) | 5(之前为6,更新为5) |
节点2(已记录不可变) | 节点0 | 3 | |
节点3 | 节点2(之前为不可达,更新为节点2) | 6(之前为∞,更新为6) | |
节点4 | 节点2(之前为不可达,更新为节点2) | 7(之前为∞,更新为7) | |
节点5 | 不可达 | ∞ |
(7)继续循环可以找到距离最小的非记录节点为节点1,源点到节点1距离为5(后边计算距离通过节点2到达其他节点需要累加距离5),记录该节点,通过该节点为跳板,找到通过节点1可以到达的节点以及计算距离,与当前存在的距离比较大小,如果小于当前存在距离,替换当前距离为刚刚计算出来的距离。
循环节点1可以到达的所有节点除去源点,除去已记录节点,为节点3。
可知通过节点1可以达到节点3,节点1到达节点3距离为5,因此节点0通过节点1到达节点3距离为5(源点到节点1距离)+5(节点1到达节点3距离)=10,是大于当前表中的距离6的,因此不更新。
本次修改完毕后,可以看到下面矩阵有所变化,到达终点节点1已记录不可变。
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1(已记录不可变) | 节点2 | 5 |
节点2(已记录不可变) | 节点0 | 3 | |
节点3 | 节点2 | 6 | |
节点4 | 节点2 | 7 | |
节点5 | 不可达 | ∞ |
(8)继续循环可以找到距离最小的非记录节点为节点3,源点到节点3距离为6(后边计算距离通过节点3到达其他节点需要累加距离6),记录该节点,通过该节点为跳板,找到通过节点3可以到达的节点以及计算距离,与当前存在的距离比较大小,如果小于当前存在距离,替换当前距离为刚刚计算出来的距离。
循环节点3可以到达的所有节点除去源点,除去已记录节点,为节点4节点5。
可知通过节点3可以达到节点4,节点3到达节点4距离为2,因此节点0通过节点3到达节点4距离为6+2=8,是大于当前表中的距离7的,因此不更新。
可知通过节点3可以达到节点5,节点3到达节点5距离为3,因此节点0通过节点3到达节点5距离为6+3=9,是小于当前表中的距离∞的,因此更新节点0到节点5的距离为9,路径修改不可达为前置节点3。
本次修改完毕后,可以看到下面矩阵有所变化
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1(已记录不可变) | 节点2 | 5 |
节点2(已记录不可变) | 节点0 | 3 | |
节点3(已记录不可变) | 节点2 | 6 | |
节点4 | 节点2 | 7 | |
节点5 | 节点3(之前为不可达,更新为节点3) | 9(之前为∞,更新为9) |
(9)继续循环可以找到距离最小的非记录节点为节点4,源点到节点4距离为7(后边计算距离通过节点4到达其他节点需要累加距离7),记录该节点,通过该节点为跳板,找到通过节点4可以到达的节点以及计算距离,与当前存在的距离比较大小,如果小于当前存在距离,替换当前距离为刚刚计算出来的距离。
循环节点4可以到达的所有节点除去源点,除去已记录节点,为节点5。
可知通过节点4可以达到节点5,节点4到达节点5距离为5,因此节点0通过节点4到达节点5距离为7+5=12,是大于当前表中的距离9的,因此不更新。
本次修改完毕后,可以看到下面矩阵有所变化
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1(已记录不可变) | 节点2 | 5 |
节点2(已记录不可变) | 节点0 | 3 | |
节点3(已记录不可变) | 节点2 | 6 | |
节点4(已记录不可变) | 节点2 | 7 | |
节点5 | 节点3 | 9 |
(10)继续循环可以找到距离最小的非记录节点为节点5,源点到节点5距离为9(后边计算距离通过节点5到达其他节点需要累加距离9),记录该节点,通过该节点为跳板,找到通过节点5可以到达的节点以及计算距离,与当前存在的距离比较大小,如果小于当前存在距离,替换当前距离为刚刚计算出来的距离。
循环节点4可以到达的所有节点除去源点,除去已记录节点,没有节点。
本次修改完毕后,可以看到下面矩阵有所变化
起点 | 终点 | 路径 | 距离 |
---|---|---|---|
节点0 | 节点1(已记录不可变) | 节点2 | 5 |
节点2(已记录不可变) | 节点0 | 3 | |
节点3(已记录不可变) | 节点2 | 6 | |
节点4(已记录不可变) | 节点2 | 7 | |
节点5(已记录不可变) | 节点3 | 9 |
(11)所有节点都已经标记,循环结束,现在可以很清楚的看出,节点0到各个节点距离了。
那么路径怎么看呢,需要倒着看,比如咱们要看节点0到节点5的路径,可以找到上表中的路径也就是前置节点是节点3,因此需要通过节点3才能到达节点5,接下来找节点3的前置节点,为节点2,因此通过节点2才能到达节点3,接下来找节点2的前置节点为节点0,也就是源点,那么路径一下就出来了,路径为节点0->节点2->节点3->节点5,距离直接到上表中查看为9。
14.图论-最小生成树-Prim算法
(1)最小生成树,在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点u与顶点v的边,而w(u,v)代表此的边权重,若存在T为E的子集(即)且为无循环图,使得的 w(T) 最小,则此 T 为 G 的最小生成树。最小生成树其实是最小权重生成树的简称。说人话就是,在一个图中,一个点与其他节点直接相连或者通过其他节点再连接到其余所有节点,最终形成一个与所有就节点都能够直接或间接相连的树,没有形成环路,且所有相连接的点之间的距离之和最小的时候,就是最小生成树,具体可以如下图,分别是原图和最小生成树:
(2)Prim算法,对图G(V,E)设置集合S,存放已访问的顶点,然后每次从集合V-S中选择与集合S的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。执行n次(n为顶点个数),直到集合S已包含所有顶点。
(3)咱们通过文字描述一遍具体的算法。上图可以写出来邻接矩阵,矩阵如下:
节点1 | 节点2 | 节点3 | 节点4 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|---|---|---|
节点1 | 0 | 23 | * | * | * | 28 | 36 |
节点2 | 23 | 0 | 20 | * | * | * | 1 |
节点3 | * | 20 | 0 | 15 | * | * | 4 |
节点4 | * | * | 15 | 0 | 3 | * | 9 |
节点5 | * | * | * | 3 | 0 | 17 | 16 |
节点6 | 28 | * | * | * | 17 | 0 | 25 |
节点7 | 36 | 1 | 4 | 9 | 16 | 25 | 0 |
(4)初始化所有节点到未访问集合中N_V:{节点1,节点2,节点3,节点4,节点5,节点6,节点7},初始化已访问集合V:{},初始化最小生成树边集合E:{}
(5)在上面图中任意选择一个点,咱就先选择节点1,放入到已访问集合V中,V:{节点1},未访问的集合N_V:{节点2,节点3,节点4,节点5,节点6,节点7},如下图,红色为已访问节点,蓝色为未访问节点。由于只有一个点,未形成边,因此无法加入边到E:{}集合中。
(6)遍历已访问节点V:{节点1}中所有节点到未访问的节点集合N_V:{节点2,节点3,节点4,节点5,节点6,节点7}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点1和节点2的距离为23,因此把节点2加入到已访问集合,在未访问集合中去掉节点2,V:{节点1,节点2},未访问的集合N_V:{节点3,节点4,节点5,节点6,节点7}。将边节点1-节点2(距离23)加入到最小生成树边集合E:{节点1-节点2(距离23)}
(7)遍历已访问节点V:{节点1,节点2}中所有节点到未访问的节点集合N_V:{节点3,节点4,节点5,节点6,节点7}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点2和节点7的距离为1,因此把节点7加入到已访问集合,在未访问集合中去掉节点7,V:{节点1,节点2,节点7},未访问的集合N_V:{节点3,节点4,节点5,节点6}。将边节点2-节点7(距离1)加入到最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1)}
(8)遍历已访问节点V:{节点1,节点2,节点7}中所有节点到未访问的节点集合N_V:{节点3,节点4,节点5,节点6}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点7和节点3的距离为4,因此把节点3加入到已访问集合,在未访问集合中去掉节点3,V:{节点1,节点2,节点3,节点7},未访问的集合N_V:{节点4,节点5,节点6}。将边节点7-节点3(距离4)加入到最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1),节点7-节点3(距离4)}
(9)遍历已访问节点V:{节点1,节点2,节点3,节点7}中所有节点到未访问的节点集合N_V:{节点4,节点5,节点6}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点7和节点4的距离为9,因此把节点4加入到已访问集合,在未访问集合中去掉节点4,V:{节点1,节点2,节点3,节点4,节点7},未访问的集合N_V:{节点5,节点6}。将边节点7-节点4(距离9)加入到最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1),节点7-节点3(距离4),节点7-节点4(距离9)}
(10)遍历已访问节点V:{节点1,节点2,节点3,节点4,节点7}中所有节点到未访问的节点集合N_V:{节点5,节点6}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点4和节点5的距离为3,因此把节点5加入到已访问集合,在未访问集合中去掉节点5,V:{节点1,节点2,节点3,节点4,节点5,节点7},未访问的集合N_V:{节点6}。将边节点4-节点5(距离3)加入到最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1),节点7-节点3(距离4),节点7-节点4(距离9),节点4-节点5(距离3)}
(11)遍历已访问节点V:{节点1,节点2,节点3,节点4,节点5,节点7}中所有节点到未访问的节点集合N_V:{节点6}中任意节点中最短的距离,通过邻接矩阵可以遍历出来是节点5和节点6的距离为17,因此把节点6加入到已访问集合,在未访问集合中去掉节点6,V:{节点1,节点2,节点3,节点4,节点5,节点6,节点7},未访问的集合N_V:{}。将边节点5-节点6(距离17)加入到最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1),节点7-节点3(距离4),节点7-节点4(距离9),节点4-节点5(距离3),节点5-节点6(距离17)}。
(13)至此未访问集合为空,最小生成树边集合E:{节点1-节点2(距离23),节点2-节点7(距离1),节点7-节点3(距离4),节点7-节点4(距离9),节点4-节点5(距离3),节点5-节点6(距离17)}不再变化,遍历最小生成树累加距离,得到最小生成树的距离和为57,也就是最小生成树的权为57,最小生成树如下:
15.图论-最小生成树-Kruskal算法
(1)最小生成树,在一给定的无向图G = (V, E) 中,(u, v) 代表连接顶点u与顶点v的边,而w(u,v)代表此的边权重,若存在T为E的子集(即)且为无循环图,使得的 w(T) 最小,则此 T 为 G 的最小生成树。最小生成树其实是最小权重生成树的简称。说人话就是,在一个图中,一个点与其他节点直接相连或者通过其他节点再连接到其余所有节点,最终形成一个与所有就节点都能够直接或间接相连的树,没有形成环路,且所有相连接的点之间的距离之和最小的时候,就是最小生成树,具体可以如下图,分别是原图和最小生成树:
(2)Kruskal算法,对图G(V,E),每次选择图中权值最小的边(在不构成环且最后能够连接完所有边它们的权重和一定是最小的)加入到集合中,直到图中所有顶点联通构成最小生成树完毕。
(3)咱们通过文字描述一遍具体的算法。上图可以写出来邻接矩阵,矩阵如下:
节点1 | 节点2 | 节点3 | 节点4 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|---|---|---|
节点1 | 0 | 23 | * | * | * | 28 | 36 |
节点2 | 23 | 0 | 20 | * | * | * | 1 |
节点3 | * | 20 | 0 | 15 | * | * | 4 |
节点4 | * | * | 15 | 0 | 3 | * | 9 |
节点5 | * | * | * | 3 | 0 | 17 | 16 |
节点6 | 28 | * | * | * | 17 | 0 | 25 |
节点7 | 36 | 1 | 4 | 9 | 16 | 25 | 0 |
(4)初始化集合E{}。
(5)遍历邻接矩阵,找到图中权值最小的边节点2-节点7,距离为1,加入到集合中E{节点2-节点7(距离1)}。
(6)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1)}中的边形成环路的边,可以找到边节点4-节点5,距离为3,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3)}。
(7)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1)}中的边形成环路的边,可以找到边节点4-节点5,距离为3,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3)}。
(8)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1),节点4-节点5(距离3)}中的边形成环路的边,可以找到边节点3-节点7,距离为4,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4)}。
(9)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4)}中的边形成环路的边,可以找到边节点4-节点7,距离为9,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4),节点4-节点7(距离9)}。
(10)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4),节点4-节点7(距离9)}中的边形成环路的边,可以找到边节点5-节点6,距离为17,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4),节点4-节点7(距离9),节点5-节点6(距离17)}。这里需要注意,虽然有小于17的距离15的节点3-节点4,但是这个边加入进去会产生环路,需要排除掉。如下图虚线:
(11)遍历邻接矩阵,找到图中权值最小的边且不与集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4),节点4-节点7(距离9),节点5-节点6(距离17)}中的边形成环路的边,可以找到边节点1-节点2,距离为23,加入到结合E中,集合E{节点2-节点7(距离1),节点4-节点5(距离3),节点3-节点7(距离4),节点4-节点7(距离9),节点5-节点6(距离17),节点1-节点2(距离23)}。
(12)此时,已经这些边已经连通,形成了最小生成树,遍历集合E,累计所有边长,得到最小生成树权值为57,最小生成树如下:
16.图论-拓扑算法
(1)拓扑算法,是一种图上排序,在一张有向无环图(有向无环图即很多参考书和题解中所说的DAG)上进行排序,把其中的所有节点排成一个序列,使得图中的任意一对有边相连的节点(u,v)u要出现在v前。说人话就是,在一张有向无环图中,也就是图中的边是有方向的,对于点来说就是有输入的边,以及输出的边,也就是有入度和出度,并且所有边没有形成环路的情况,这种图的点排列起来,排列要按照一个边的两个点,分别指入度点和出度点,所有的出度点要排在入度点前面形成的一个序列。所以要注意再注意一定是有向无环图才能做拓扑排序。
(2)有向无环图如下:
(3)可以定义,当前节点有到其他节点的出度,则为节点值为1,当前节点有从其他节点来的入度边,则节点值为-1,都没有则为0。画出矩阵如下:
节点1 | 节点2 | 节点3 | 节点4 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|---|---|---|
节点1 | 0 | -1 | 0 | 0 | 0 | 1 | -1 |
节点2 | 1 | 0 | 1 | 0 | 0 | 0 | 1 |
节点3 | 0 | -1 | 0 | 1 | 0 | 0 | 1 |
节点4 | 0 | 0 | -1 | 0 | 1 | 0 | 1 |
节点5 | 0 | 0 | 0 | -1 | 0 | 0 | -1 |
节点6 | -1 | 0 | 0 | 0 | -1 | 0 | -1 |
节点7 | 1 | -1 | -1 | -1 | 1 | 1 | 0 |
(4)由上图矩阵可知,入度为0的节点,就是上图的a[i][0-6]中没有-1节点,也就是节点2所在行,也就是a[1][0],a[1][1],a[1][2],a[1][3],a[1][4],a[1][5],a[1][6]这一行都没有值为-1的节点,这是咱们直接看出来的,需要数组循环遍历得到,下面开始具体算法。
(5)遍历数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点2,将节点2加入到拓扑排序的集合E:{节点2},在矩阵中删除节点2,修改矩阵如下:
节点1 | 节点3 | 节点4 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|---|---|
节点1 | 0 | 0 | 0 | 0 | 1 | -1 |
节点3 | 0 | 0 | 1 | 0 | 0 | 1 |
节点4 | 0 | -1 | 0 | 1 | 0 | 1 |
节点5 | 0 | 0 | -1 | 0 | 0 | -1 |
节点6 | -1 | 0 | 0 | -1 | 0 | -1 |
节点7 | 1 | -1 | -1 | 1 | 1 | 0 |
(6)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点3,将节点3加入到拓扑排序的集合E:{节点2,节点3},在矩阵中删除节点3,修改矩阵如下:
节点1 | 节点4 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|---|
节点1 | 0 | 0 | 0 | 1 | -1 |
节点4 | 0 | 0 | 1 | 0 | 1 |
节点5 | 0 | -1 | 0 | 0 | -1 |
节点6 | -1 | 0 | -1 | 0 | -1 |
节点7 | 1 | -1 | 1 | 1 | 0 |
(7)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点4,将节点4加入到拓扑排序的集合E:{节点2,节点3,节点4},在矩阵中删除节点4,修改矩阵如下:
节点1 | 节点5 | 节点6 | 节点7 | |
---|---|---|---|---|
节点1 | 0 | 0 | 1 | -1 |
节点5 | 0 | 0 | 0 | -1 |
节点6 | -1 | -1 | 0 | -1 |
节点7 | 1 | 1 | 1 | 0 |
(8)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点7,将节点7加入到拓扑排序的集合E:{节点2,节点3,节点4,节点7},在矩阵中删除节点7,修改矩阵如下:
节点1 | 节点5 | 节点6 | |
---|---|---|---|
节点1 | 0 | 0 | 1 |
节点5 | 0 | 0 | 0 |
节点6 | -1 | -1 | 0 |
(9)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点1,将节点1加入到拓扑排序的集合E:{节点2,节点3,节点4,节点7,节点1},在矩阵中删除节点1,修改矩阵如下:
节点5 | 节点6 | |
---|---|---|
节点5 | 0 | 0 |
节点6 | -1 | 0 |
(10)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点5,将节点5加入到拓扑排序的集合E:{节点2,节点3,节点4,节点7,节点1,节点5},在矩阵中删除节点5,修改矩阵如下:
节点6 | |
---|---|
节点6 | 0 |
(11)遍历新数组每一行,取得改行所有节点没有值为-1的行,得到该节点,可以得到节点6,将节点6加入到拓扑排序的集合E:{节点2,节点3,节点4,节点7,节点1,节点5,节点6},在矩阵中删除节点6,至此数组为空,所有节点都加入到集合E中,序列即位集合E的顺序,得到拓扑排序序列:节点2,节点3,节点4,节点7,节点1,节点5,节点6。
17.字符串查找-Knuth-Morris-Pratt算法
(1)KMP算法的主要思想是当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配,直接跳过一部分已知内容后再继续匹配。
(2)先来看下正常的匹配逻辑,在字符串:abcabghabcabc(目标串)中搜索abcabc(搜索条件串)字符
初始化两个字符串的指针i,j,从第一个字符开始匹配,可以匹配成
i
a b c a b g h a b c a b c
a b c a b c
j
匹配第二个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
匹配第三个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
匹配第四个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
匹配第五个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
匹配第六个字符,失败
i
a b c a b g h a b c a b c
a b c a b c
j
搜索条件串右移,重新开始匹配,第一个字符匹配失败
i
a b c a b g h a b c a b c
a b c a b c
j
搜索条件串右移,重新开始匹配,第一个字符匹配失败
i
a b c a b g h a b c a b c
a b c a b c
j
如此往复匹配
(3)上面的算法,效率不高,因此有人提出了KMP算法,记录已经匹配的内容,也就是通过next数组的下表来跳过相应的字符数。但是如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。
(4)next数组如何计算呢?需要重点关注字符串的前后缀,字符串的前缀是指字符串的任意首部,比如字符串“abcabc”的前缀有“a””,“ab””,“abc”,“abca”,“abcab”,字符串的任意尾部是字符串的后缀,“abcabc”的后缀有“c”,“bc”,“abc”,“cabc”,“bcabc”。
(5)next数组的每一位下标实际上就是搜索条件串(abcabc)截止到当前位公共的前后缀最大长度,咱们来写一下。
(6)搜索条件串(abcabc)第一位是a,前缀后缀都没有,所以公共前后缀最大长度为0
(7)接下来搜索条件串(abcabc)前两位是ab,前缀为a,后缀为b,没有公共字符,所以公共前后缀最大长度为0
(8)接下来搜索条件串(abcabc)前三位是abc,前缀为a,ab,后缀为c,bc,没有公共字符,所以公共前后缀最大长度为0
(9)接下来搜索条件串(abcabc)前四位是abca,前缀为a,ab,abc,后缀为a,ca,bca,存在公共字符串a,所以公共前后缀最大长度为1
(10)接下来搜索条件串(abcabc)前五位是abcab,前缀为a,ab,abc,abca,后缀为b,ab,cab,bcab,存在公共字符串ab,所以公共前后缀最大长度为2
(11)接下来搜索条件串(abcabc)前六位是abcabc,前缀为a,ab,abc,abca,abcab,后缀为c,bc,abc,cabc,bcabc,存在公共字符串abc,所以公共前后缀最大长度为3
(12)因此可以直接写出来,next数组下标为
a b c a b c
0 0 0 1 2 3
(13)接下来正式开展,KMP算法的神奇之处了,咱们重新匹配两个字符串
初始化两个字符串的指针i,j,从第一个字符开始匹配,可以匹配成
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移,匹配第二个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移,匹配第三个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移,匹配第四个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移,匹配第五个字符成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移,匹配第六个字符,失败
i
a b c a b g h a b c a b c
a b c a b c
j
0 0 0 1 2 3
此时可以看到之前已经匹配成功的字符下标为2,因此搜索条件串右移3位(普通算法右移1位,但是通过下标为2,多增加2位,也就是一共右移3位),KMP算法的厉害之处在于指针i,j没有返回,其实我知道您肯定会问,为什么右移3位,其实可以看一下,上面匹配到第六个字符失败的时候,是不是第五位和第四位的ab已经匹配完成了,那么我为什么还要重新在匹配呢?直接跳过就好了,因此直接右移到目标串的对应位置即可,也就是直接右移三位,跳过已经匹配过的字符,去匹配新的字符,因此比较目标串第六位和搜索条件串的第三位即可,失败
i
a b c a b g h a b c a b c
a b c a b c
j
0 0 0 1 2 3
已经匹配的字符的next数组下标为0,搜索条件串右移一位重新匹配,指针i,j没有移动,仍然失败
i
a b c a b g h a b c a b c
a b c a b c
j
0 0 0 1 2 3
已经匹配的字符的next数组下标为0,搜索条件串右移一位重新匹配,指针i,j没有移动,仍然失败
i
a b c a b g h a b c a b c
a b c a b c
j
0 0 0 1 2 3
已经匹配的字符的next数组下标为0,搜索条件串右移一位,此时指针i,j均开始右移一位,重新匹配,仍然失败
i
a b c a b g h a b c a b c
a b c a b c
j
搜索条件串右移一位,此时指针i,j均右移一位,重新匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,继续匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,继续匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,继续匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,继续匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,继续匹配,成功
i
a b c a b g h a b c a b c
a b c a b c
j
指针i,j右移一位,搜索条件串没有字符了,说明找到了对应的字符串,算法执行完毕
i
a b c a b g h a b c a b c
a b c a b c
j
(13)通过以上算法可以明确的理解KMP算法的具体步骤,以及next数据的算法,另外为什么按照next数组的下标去右移。
18.字符串查找-Boyer-Moore算法
(1)Boyer-Moore字符串搜索算法是一种非常高效的字符串搜索算法
(2)BM算法采用从右向左比较的方法,应用到了两种启发式规则,即坏字符规则和好后缀规则,来决定向右跳跃的距离。
(3)匹配的过程中,取坏字符规则和好后缀规则中跳跃的较大者作为跳跃的距离。
(4)比较过程中,若是某次比较不匹配时,BM算法就采用坏字符规则和好后缀规则,来计算模式串向右移动的距离,直到整个匹配过程的结束。
(5)第一个不匹配的字符(下面内容中的b字符)为坏字符,已匹配部分(下面内容中的cab字符)为好后缀,如下
目标串T = abcecabe
搜索条件串P = abcab
逐个匹配,向右移动到如下所示情况
a b c e c a b e
a b c a b
(6)坏字符串匹配规则,从右向左匹配,匹配搜索条件串从右向左数第一个字符b与目标串对应的字符c,这两个字符不匹配,但是目标串的字符c在搜索条件串中存在,因此直接跳到对应的位置,如下部分
匹配字符b与字符c,不匹配,但是目标串的c在搜索条件串中
a b c e c a b e
a b c a b
因此直接跳转到搜索条件串的对应位置,也就是搜索条件串右移2位
a b c e c a b e
a b c a b
(7)坏字符串匹配规则,从右向左匹配,匹配搜索条件串从右向左数第一个字符b与目标串对应的字符e,这两个字符不匹配,目标串的字符e在搜索条件串中不存在,说明了从字符e以前的均不会匹配成功,没有必要做无用的匹配操作,直接全部跳过
匹配字符b与字符e,不匹配,但是目标串的e不在搜索条件串中
a b c e c a b e
a b a b
因此直接跳转到搜索条件串的对应位置,也就是搜索条件串右移搜索条件串长度4位
a b c e c a b e
a b a b
(8)用数学公式表示,上面的算法,设skip(x)为搜索条件串P右移的距离,m为搜索条件串P的长度,max(x)为字符x在P中最右位置,具体计算右移长度的公式如下:
(9)好后缀匹配规则,如果模式串从前往后存在与好后缀相同的字串,就将字串与该好后缀对齐,若有多个则选择最后面那个,如果选择不是最后的会过度移。具体如下:
比较到搜索条件串从右向左数,第三个字符不匹配
存在好后缀ab,在搜索条件串中存在ab
a b c a a b b e
a b a b a b
找到最后一个ab对齐,移动两位如下
a b c a a b b e
a b a b a b
(10)好后缀匹配规则,如果搜索条件串从前往后不存在与好后缀相同的字串,那么直接跳过整个搜索条件串的长度即可
比较到搜索条件串从右向左数,第三个字符不匹配
存在好后缀ab,在搜索条件串中不存在ab
a b c a a b b e
a a c a b
直接跳过全部长度,移动五位如下
a b c a a b b e
a a c a b
几种算法思想
19.算法-递归
(1)递归算法是一种直接或者间接调用自身函数或者方法的算法。说简单了就是程序自身的调用。
(2)递归算法就是将原问题不断分解为规模缩小的子问题,然后递归调用方法来表示
问题的解。(用同一个方法去解决规模不同的问题)
(3)具体看一下测试代码,咱们用斐波那契额数列(除了前两个值,后边每个值等于这个值前两个数字的和)的例子,1,1,2,3,5,8,13,21,34,55,89...
public class TestRecursion {
public static int fibonacci (int i) {
// 初始值,总共1个兔子
if(i == 0){
return 1;
// 1月底,总共1个兔子
} else if(i == 1){
return 1;
}
// 递归调用本方法,并把前两次的累加起来,获取i月底兔子总数
return fibonacci(i-1) + fibonacci(i-2);
}
public static void main(String[] args) {
System.out.println(fibonacci(8));
}
}
//输出
34
(4)从上边代码可以看出来,调用fibonacci(8)实际上又在内部调用了fibonacci(7)和fibonacci(6),fibonacci(7)又调用了fibonacci(6)和fibonacci(5),依次递归调用,直到最终调用fibonacci(1)和fibonacci(2)分别直接返回值1。
20.算法-递推
(1)是指从己知条件出发,逐步推算出要解决问题的方法,也叫顺推法。或者是从己知的结果出发,用迭代表达式逐步推算出问题开始的条件,即顺推法的逆过程,即逆推法。
(2)比如说兔子数列——斐波那契数列,如下图:
(3)写出图来,可以看出,每个月的兔子个数都是前两个月兔子数的和,按照递推公式也就是f(n)=f(n-1)+f(n-2),一个非常容易的数学公式。
(4)接下来就可以写代码了,具体代码如下:
import java.util.ArrayList;
import java.util.List;
public class TestRecursion {
// 初始化list,初始状态,总共1个兔子
// 1月底,总共1个兔子
static List<Integer> list = new ArrayList<>();
static {
// 放入0只,为了设置月数第1月底为1,这个只为了填充list的0
list.add(0);
// 初始值,1
list.add(1);
// 1月底,1
list.add(2);
}
public static int fibonacci (int month) {
// 获取初始状态和1月底,都直接返回兔子总数
if(month <= 2){
return list.get(month);
}
// 循环递推计算第month月底,总共多少只兔子
for(int i=3; i <= month; i++){
int rabbitCount = list.get(i-2) + list.get(i-1);
list.add(rabbitCount);
}
// 返回第month月底,总共多少只兔子
return list.get(month);
}
public static void main(String[] args) {
System.out.println(fibonacci(8));
}
}
21.算法-贪心
(1)贪心算法就是采用贪心的算法思想,保证每次操作都是局部最优,从而保证最后结果是全局最优的。
(2)前面讲的最短路径的求法,每次都是取路径中最短的那一条边,依次取下来,直到所有遍都连通的情况结束,这种就是一种非常清晰的贪心算法。
22.算法-枚举
(1)枚举算法我们也称之为穷举算法,遍历所有情况来匹配,这种算法就是在解决问题的时候去使用所有的方式去解决这个问题,会通过推理去考虑事件发生的每一种可能,最后得出结论。
(2)虽然枚举算法非常暴力,而且速度可能很慢,但确实我们最应该优先考虑的!因为枚举法变成实现最简单,并且得到的结果总是正确的。
(3)可以简单写一个水仙花算法,首先我们来了解一下什么是水仙花数,所谓"水仙花数"是指一个三位数,其各位数字立方和等于该本身。例如:153是一个水仙花数,因为153=1^3+5^3+3^3。
public static void main(String[] args) {
for (int i = 100; i < 1000; i++) {
int a = i / 100;
int b = i / 10 % 10;
int c = (i % 10);
if ((Math.pow(a, 3) + Math.pow(b, 3) + Math.pow(c, 3)) == i) {
System.out.println("水仙花:" + i);
}
}
}
//输出
水仙花:153
水仙花:370
水仙花:371
水仙花:407
23.算法-动态规划
(1)动态规划的思想简单来说就是把递归的问题转化为递推的问题,把需要递归的算法,转化为不需要递归的算法,即用空间去存储前一步的结果,供后面使用。其实动态规划算一种空间换时间的算法。
(2)说人话就是,根据给出的问题,写出一个递推的计算公式,然后通过这个计算公式写出具体的算法代码。
(3)我们来看个具体的算法题目来了解动态规划,一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
(4)思路来了啊,设定每间的金额存在数组num[i]中,如果小偷从第一间房屋开始偷,并且偷到第n - 1间时(还没偷第n - 1间),它偷的总金额的最大值为sum[n - 2](意思是0至n - 2间中不挨着的房间都偷完成了时的最大金额,不关心第n - 2间是否偷了)。那它偷到第n间时(还没偷第n间),它偷的总金额最大是多少呢?
(5)到这想一想,倒过来想,应该是sum[n],那么sum[n]等于什么呢?倒过来想一想,实际上,只有两种偷的方法,第一种,n偷了,第二种,n没偷。
(6)如果n偷了,那么偷的总金额是在第n间偷的金额加上偷的第n - 2间的总和sum[n - 2](因为n偷了,所以n - 1肯定没偷,所以不应该是sum[n - 1]),这两个的和,也就是num[n]+sum[n - 2]。
(7)如果n没偷,那么实际就是前n - 1总共偷的最大的和,也就是sum[n - 1]。
(8)到现在为止,想没想出来,比较这两个的大小,n偷了的总值与n没偷的总值谁大,所以sum[n] = max(num[n]+sum[n - 2] , sum[n - 1])。
(9)这个公式一出来,代码就可以写出来了,具体如下:
import java.util.HashMap;
public class TestDynamicMethod {
public static void main(String[] args) {
int[] nums1 = { 1, 2, 3, 1 };
System.out.println(dynamicMethod(nums1));
int[] nums2 = { 2, 7, 9, 3, 1 };
System.out.println(dynamicMethod(nums2));
}
public static int dynamicMethod(int[] nums) {
int len = nums.length;
if (len == 0)
return 0;
if (len == 1)
return nums[0];
HashMap sum = new HashMap();
sum.put(0, nums[0]);
sum.put(1, Math.max(nums[0], nums[1]));
// 按照咱们总结的递推公式来挨个计算
for (int i = 2; i < len; i++) {
sum.put(i, Math.max(nums[i] + (int) sum.get(i - 2), (int) sum.get(i - 1)));
}
return (int) sum.get(len - 1);
}
}
//输出
4
12
24.算法-回溯法
(1)回溯法简单来说就是按照深度优先的顺序,穷举所有可能性的算法,但是回溯算法比暴力穷举法更高明的地方就是回溯算法可以随时判断当前状态是否符合问题的条件。一旦不符合条件,那么就退回到上一个状态,省去了继续往下探索的时间。
(2)回溯是递归的副产品,只要有递归就会有回溯。也就是说,递归时逐个进入子逻辑,回溯时逐个从子逻辑返回。
(3)回溯法的伪代码逻辑如下,一看就能明白其中逻辑
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
(4)咱们来看一个例子,给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按任何顺序返回答案。
示例 1:
输入:n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
示例 2:
输入:n = 1, k = 1
输出:[[1]]
(5)先按照正常逻辑来思考下,返回从1到n的范围内中取出k个数的组合,上面示例1中,从1到4中取出2个数的组合,通过2层循环,分别取出,代码为:
public class Test {
public static void main(String[] args) {
int n = 4;
List<String> list = new ArrayList<>();
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
list.add(i + " " + j);
}
}
// 输出打印
list.forEach(System.out::println);
}
}
//输出
1 2
1 3
1 4
2 3
2 4
3 4
(5)那么从1到100中取出3个数的组合,通过3层循环,分别取出,代码为:
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int n = 10;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
for (int k = j + 1; k <= n; k++) {
list.add(i + " " + j + " "+ k);
}
}
}
// 输出打印
list.forEach(System.out::println);
}
}
//输出
1 2 3
1 2 4
1 2 5
1 2 6
1 2 7
1 2 8
1 2 9
1 2 10
1 3 4
//还很多,不一一打印了
(6)这样,如果选出的数字越多,复杂度会按照指数级增加。所以咱们再次按照迭代的思想思考下,怎么办?有一个思路,按照1到100中取出3个数的组合,当我取出第一个数1时,那么按照迭代的算法,第一个数已经确定,那么需要在剩下的2-100中取出2个数,然后进入回溯方法,第二个数取2,那么需要在剩下的3-100种取出1个数,就完成,遍历3-100取出结果,把前面的1,2和遍历的结果组合即可。
public class Test {
private static List<String> list = new ArrayList<>();
private static String countArr = "";
public static void main(String[] args) {
int n = 100;
backtracking(1, n, 3);
// 输出打印
list.forEach(System.out::println);
}
private static void backtracking(int start, int n, int toGetCnt) {
// 终止条件
if (toGetCnt == 0) {
return;
}
// 本次集合中的元素
for (int j = start; j <= n; j++) {
// 处理节点
String toAddStr = " " + j;
countArr = countArr + toAddStr;
// 符合条件加入list
if (toGetCnt == 1) {
list.add(countArr);
}
// 递归
backtracking(j + 1, n, toGetCnt - 1);
// 回溯清理并删除这一层增加的内容
countArr = countArr.substring(0, countArr.length() - toAddStr.length());
}
}
}
//输出,有很多,不一一输出了
96 97 100
96 98 99
96 98 100
96 99 100
97 98 99
97 98 100
97 99 100
98 99 100
25.算法-分治
(1)分治算法,字面上的解释是“分而治之”,将一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题----“分”,将最后子问题可以简单的直接求解----“治”,将所有子问题的解合并起来就是原问题打得解----“合”。
(2)说人话就是,把一个复杂的问题,拆分成几个小的问题,分别处理,最后再把几个小问题的处理结果合并在一起处理的过程。
(3)常见的分治算法有很多,比如二分搜索,合并排序,快速排序等等。
(4)咱们可以按照二分搜索(注意搜索的数据都是排好序的)的例子来学习一下代码:
public class BinarySearch {
public static void main(String[] args) {
// 初始化数据
int arr[] = new int[20];
for (int i = 0; i < 20; i++) {
arr[i] = i;
}
// 开始处理
int result = bindarySearch(arr, 0, arr.length - 1, 21);
System.out.println(result);
}
private static int bindarySearch(int arr[], int low, int high, int target) {
// 当处理到最后一次,如果已经达到最大或最小时,仍然不是返回错误-1
if (low == high && target != arr[low]) {
return -1;
}
// 计算中位值
int mid = (low + high) / 2;
// 比较目标与中位值大小,相等返回目标
if (target == arr[mid]) {
return target;
// 目标大于中位值,二分当前数组,并到右侧大的一部分数组继续查找
} else if (target > arr[mid]) {
return bindarySearch(arr, mid + 1, high, target);
// 目标小于中位值,二分当前数组,并到左侧小的一部分数组继续查找
} else {
return bindarySearch(arr, low, mid - 1, target);
}
}
}
必学十大排序算法
26.排序算法-选择排序
(1)选择排序,顾名思义,从左到右遍历,每次选择最小的一个,放到第一个,之后从第二个元素开始遍历,再次选出最小的,放到第二个,接下来从第三个元素开始遍历,选出最小的,放到第三个,直到遍历到最后一个元素,此时已经排序完成。
(2)也可以从左向右遍历,选择最大的,放到最后一个元素,之后再从左到右遍历至倒数第二个元素,依次遍历,同样也可以。
(3)选择排序是不稳定的排序方法。稳定不稳定的意思就是在排序过程中,如果两个键的值相同,那么他们的相对位置不发生变化,不符合该规则的排序算法是不稳定排序算法。
(4)接下来看排序代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 选择排序
*/
public class SelectionSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 100; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s+"-"));
System.out.println("");
nums = selectionSort(nums);
nums.forEach(s -> System.out.print(s+"-"));
}
public static List selectionSort(List nums) {
// 循环遍历取出每次的最小值
for (int i = 0; i < nums.size(); i++) {
// 设定最小值
int min = Integer.MAX_VALUE;
// 初始化最小值位置
int index = -1;
for (int j = i; j < nums.size(); j++) {
// 如果当前遍历到的值比最小值小则设定最小值为当前遍历到的值,以及记录位置index
if ((int)nums.get(j) < min) {
min = (int)nums.get(j);
index = j;
}
}
// 修改本次遍历的列表中的首位与最小值的位置
int oldFirst = (int)nums.get(i);
nums.set(i, min);
nums.set(index, oldFirst);
}
return nums;
}
}
27.排序算法-插入排序
(1)插入排序,从左到右遍历,首先选中第一个元素,前面没有元素,保持不变。
(2)接下来遍历到第二个元素,将第二个元素与第一个元素比较,如果大于则保持当前位置不变,本次完成;如果小于第一个元素,则将该元素插入到第一个元素前面,此时此元素已经到了第一个元素位置,前面没有元素,本次完成。
(3)继续遍历到第三个元素,将第三个元素,与第二个元素比较,如果大于第二个元素,则保持该位置不变,本次完成,如果小于第二个元素,则将该元素插入到第二个元素前面,继续与第一个元素比较,如果大于第一个元素,则保持该位置不变,本次完成,如果小与第一个元素,则将该元素插入到第一个元素前面,此时此元素已经到了第一个元素位置,前面没有元素,本次完成。
(4)依次遍历所有元素,直至最后一个元素找插入到最后一个大于该元素的元素前面。
(5)选择排序是稳定的排序。
(6)接下来看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 插入排序
*/
public class InsertionSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 100; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s+"-"));
System.out.println("");
nums = insertionSort(nums);
nums.forEach(s -> System.out.print(s+"-"));
}
public static List insertionSort(List nums) {
for (int i = 1; i < nums.size(); i++) {
for (int j = i; j >= 1; j--) {
// 比较当前位置与前一个位置大小,如果当前数值小与前一个数值,则将当前数值插入到前面
if ((int)nums.get(j) < (int)nums.get(j-1)) {
int old = (int)nums.get(j-1);
nums.set(j-1, (int)nums.get(j));
nums.set(j, old);
}
}
}
return nums;
}
}
28.排序算法-冒泡排序
(1)冒泡排序是一种简单的排序算法,它也是一种稳定排序算法。
(2)从左到右遍历,比较每一对相邻的元素,当该对元素顺序不正确时进行交换。一直重复这个过程,直到没有任何两个相邻元素可以交换,就表明完成了排序。
(3)说人话,就是类似于冒泡,从左到右,逐个比较当前元素与下一个元素大小,如果小于下一个元素,保持不变,该对元素比较结束,如果当前元素大于下一个元素,则该元素与下一个元素换位置(就像冒泡一样)。之后再与新的下一个元素比较,比较方式保持一致。
(4)上代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 100; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s+"-"));
System.out.println("");
nums = bubbleSort(nums);
nums.forEach(s -> System.out.print(s+"-"));
}
public static List bubbleSort(List nums) {
for (int i = 0; i < nums.size(); i++) {
for (int j = i+1; j < nums.size(); j++) {
// 比较j元素与i元素大小,如果i元素大与j元素,则互换位置
if ((int)nums.get(j) < (int)nums.get(i)) {
int large = (int)nums.get(i);
nums.set(i, nums.get(j));
nums.set(j, large);
}
}
}
return nums;
}
}
29.排序算法-希尔排序
(1)希尔排序是针对简单插入排序时遇到的一些问题,例如当数组 arr = {2,3,4,5,6,1} 这时需要插入的数 1(最小),这样的过程是:
{2,3,4,5,6,1}
{2,3,4,5,1,6}
{2,3,4,1,5,6}
{2,3,1,4,5,6}
{2,1,3,4,5,6}
{1,2,3,4,5,6}
(2)因此当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。
(3)希尔排序是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
(4)希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序,随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
(5)具体来看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 希尔排序
*/
public class ShellSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 9; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = shellSort(nums);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List shellSort(List nums) {
// 初始化第一次分两组
int group = 2;
// 循环增加分组数,逐渐降低比较大小的两个索引位置的间隙gap
// 遍历完一次,增加一次group大小,也就是降低比较大小的两个索引位置的间隙gap
for (int gap = nums.size() / group; gap >= 1; group *= 2,gap = nums.size() / group) {
// 从左到右开始遍历数组
for (int i = 0; i < nums.size() && i + gap < nums.size(); i++) {
// 比较i元素与i+gap元素大小,如果i元素大与i+gap元素,则互换位置
if ((int) nums.get(i+gap) < (int) nums.get(i)) {
int large = (int) nums.get(i);
nums.set(i, nums.get(i+gap));
nums.set(i+gap, large);
}
}
}
return nums;
}
}
30.排序算法-归并排序
(1)归并排序(Merge sort)算法是采用分治法的一个非常典型的应用。该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)。实际上就是如图具体的步骤。
(2)具体步骤实际上就是把整个列表拆分成若干个小的分组,直到拆分成每一组只有一个元素为止,我的实际做法是在此时直接返回当前元素,因为一个元素没有办法比较,实际只有大于等于2个元素才能比较并替换大小元素位置。
(3)当从最底层递归的只有一个元素返回到上一层递归方法后,此时存在了两个元素,例如23和35这一组,比较两个元素的大小,并按照从小到大排列好两个元素的顺序为23,35,然后继续返回上一层递归方法。
(4)返回到这一层递归方法后,其中的元素多了,此时每一组有2个元素,例如{23,35}和{12,34}这两组元素比较,分别取出每一组的第一个比较大小,将较小的插入到新列表首位,也就是23和12比较,将12插入到新列中{12},接下来已经取出的这一组取下一个值与另一组元素的首位比较,也就是23和34比较,将23插入到新列表中{12,23},接下来比较35和34,将34插入到新列表中{12,23,34},这样右边这一组取完所有值了,接下里直接把左边这一组剩下的值直接逐个放入到新列表中,结果为{12,23,34,35},返回到上一次递归中。
(5)这样直到最后跳出递归方法,全部元素已经排序完成了。
(6)接下来看代码如下:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 归并排序
*/
public class MergeSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 10; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = mergeSort(nums, 0, nums.size() - 1);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List mergeSort(List nums, int startIndex, int endIndex) {
// 说明拆分到当前list只有一个值,不需要比较直接返回,只有两个值以上才开始比较并按照大小修改顺序
if (startIndex == endIndex) {
return nums;
}
// 计算拆分位置
int mid = (startIndex + endIndex) / 2;
mergeSort(nums, startIndex, mid);
mergeSort(nums, mid + 1, endIndex);
// 开始比较从startIndex到endIndex位置
int i = startIndex;
int j = mid + 1;
List newNums = new ArrayList();
// 分别比较左部分(从startIndex到mid)与右部分(从mid+1到endIndex)
while (i <= mid && j <= endIndex) {
// 分别从左部分逐个取出与右部分逐个取出,分别将较小的放到新列表newNums中
if ((int) nums.get(i) > (int) nums.get(j)) {
newNums.add(nums.get(j));
j++;
} else {
newNums.add(nums.get(i));
i++;
}
}
// 上面循环完成后,如果左部分没有循环完成,将左部分的所有值逐个放入到新列表newNums中
while (i <= mid) {
newNums.add(nums.get(i));
i++;
}
// 上面循环完成后,如果右部分没有循环完成,将右部分的所有值逐个放入到新列表newNums中
while (j <= endIndex) {
newNums.add(nums.get(j));
j++;
}
// 遍历将新列表newNums的值插入到对应老列表nums的位置中替换
for (int k = startIndex,l=0; k <= endIndex; k++ , l++) {
nums.set(k, newNums.get(l));
}
return nums;
}
}
31.排序算法-快速排序
(1)快速排序是一个既高效又不浪费空间的一种排序算法。
(2)咱们来直接讲算法步骤,例如一个随机数列,5 1 3 8 2 4 0 6 7 10 9。
(3)首先在这个序列中随便找一个数作为基准数,一般选择第一个数,也就是5作为基准。
(4)设置一个索引值j,从右到左逐个选择一个,第一个选择了9,与基准书5比较,9大于5,则跳过直接下一个,选择10,大于5,继续跳过直接下一个,选择7,大于5,继续跳过直接下一个,选择6,大于5,继续跳过直接下一个,选择0,小于5,记住这个当前j的位置,也就是当前列表索引位置。
5 1 3 8 2 4 0 6 7 10 9
j
(5)接下来,设置一个索引值i,从左往右逐个选择(跳过基准数5的位置),逐个与基准数5比较大小,选择第一个1,小于5,继续跳过直接下一个,选择3,小于5,继续跳过直接下一个,选择8,大于5,此时i索引位置如下:
5 1 3 8 2 4 0 6 7 10 9
i j
(6)此时替换i与j位置的值,使得大于基准数5的在右边,小于基准数5的在左边。
5 1 3 0 2 4 8 6 7 10 9
i j
(7)继续索引j的向左移动,接下来选择了4,与基准数5比较,小于5,记住索引j位置。
5 1 3 0 2 4 8 6 7 10 9
i j
(8)继续索引i的向右移动,接下来选择2,与基准数5比较,小于5,继续跳过直接下一个,选择4,此时注意,i和j的位置相同了。
5 1 3 0 2 4 8 6 7 10 9
j
i
(9)此时可以看出来了索引i/j的位置很特殊,现在索引i/j左边的数,除了基准数自己,都小于5,索引i/j右边的数,都大于5。因此需要替换i/j的数和基准数的位置后,此时索引i/j左边的数都小于5,右边的数都大于5。
4 1 3 0 2 5 8 6 7 10 9
j
i
(10)接下来,5的位置,可以把这个数列拆成了两个数列,4 1 3 0 2和 8 6 7 10 9,接下来通过分治的方式,分别对左边数列后右边数列继续上面同样的步骤,4 1 3 0 2选择4作为基准数比较,8 6 7 10 9选择8作为基准数比较。
(11)直到每个序列拆分到只有一个数的时候,说明已经是正确的顺序了,返回即可。排序完成。
(12)接下来看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 快速排序
*/
public class QuickSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 10; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = quickSort(nums, 0, nums.size() - 1);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List quickSort(List nums, int startIndex, int endIndex) {
if (startIndex == endIndex || startIndex > endIndex) {
return nums;
}
int selectIndex = startIndex;
int j = endIndex;
int i = startIndex + 1;
while (i < j) {
// 从右向左逐个与基准数比较,找到小于基准数的j位置
while ((int) nums.get(j) >= (int) nums.get(selectIndex) && i < j) {
j--;
}
// 从左向右逐个与基准数比较,找到大于基准数的j位置
while ((int) nums.get(i) <= (int) nums.get(selectIndex) && i < j) {
i++;
}
// 如果i与j相等,说明找到了基准数的正确位置
if (i == j) {
break;
}
// 右边小于基准数的值与左边大于基准数的值替换位置
swap(nums, j, i);
}
// 内部遍历完成,替换基准数与i/j的数值
swap(nums, selectIndex, i);
// 分治,在i/j的位置拆分为两组,i/j左边一组,右边一组,分别快速排序
quickSort(nums, startIndex, i - 1);
quickSort(nums, i + 1, endIndex);
return nums;
}
/**
* 交换位置
* @param nums
* @param left
* @param right
*/
public static void swap(List nums, int left, int right) {
int current = (int)nums.get(left);
nums.set(left, nums.get(right));
nums.set(right, current);
}
}
32.排序算法-计数排序
(1)计数排序,只适用于整数并且最大值与最小值差值较小的情况。
(2)具体算法为,例如这个数列a[]为{9, 5, 3, 4, 9, 1, 2, 7, 7,1,4, 6, 1, 3, 4, 9, 7, 9, 10, 0},取得最大值与最小值,计算最大值与最小值的差值,根据差值大小申请一个空间来存储所有去除重复后的数列,最大值10,最小值0,那么存储的范围就是0-10,因此申请一个可以存储11个值的数组空间,arr[],并且每个的初始值都为0。
(3)遍历整个数列,第一个值为9,那么在空间arr[10]的数值+1,第二个值为5,那么在空间arr[6]的数值+1,第三个值为3,那么在空间arr[4]的数值+1,第四个值为4,那么在空间arr[5]的数值+1,第五个值为9,那么在空间arr[10]的数值+1,当前值就变成2了,直至遍历到最后,其中需要注意,原始数列中没有8,因此arr[9]的数值为0,最终得到arr[]数组为:
{1, 3, 1, 2, 3, 1, 1, 3, 0,4,1}
(4)再从arr[]数组逐个取值,最小值为0,因此逐个取值排序,arr[0]=1,因此修改a[0] = 0,其他位保持不变,也就是
{0, 5, 3, 4, 9, 1, 2, 7, 7, 1, 4, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(5)arr[1]=3,因此从第二位开始修改3位为1,a[1] = 1,a[2] = 1,a[3] = 1,也就是
{0, 1, 1, 1, 9, 1, 2, 7, 7, 1, 4, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(6)arr[2]=1,因此在第五位开始修改1位为2,a[4] = 2,也就是
{0, 1, 1, 1, 2, 1, 2, 7, 7, 1, 4, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(7)arr[3]=2,因此在第六位开始修改2位为3,a[5] = 3,a[6] = 3,也就是
{0, 1, 1, 1, 2, 3, 3, 7, 7, 1, 4, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(8)arr[4]=3,因此在第八位开始修改3位为4,a[7] = 4,a[8] = 4,a[9] = 4,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 4, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(9)arr[5]=1,因此在第十一位开始修改1位为5,a[10] = 5,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(10)arr[6]=1,因此在第十二位开始修改1位为6,a[11] = 6,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 1, 3, 4, 9, 7, 9, 10, 0}
(11)arr[7]=3,因此在第十三位开始修改1位为7,a[12] = 7,a[13] = 7,a[14] = 7,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 7, 7, 7, 9, 7, 9, 10, 0}
(12)arr[8]=0,因此数列中没有8,无需修改直接跳过
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 7, 7, 7, 9, 7, 9, 10, 0}
(13)arr[9]=4,因此在第十六位开始修改4位为9,a[15] = 9,a[16] = 9,a[17] = 9,a[18] = 9,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 7, 7, 7, 9, 9, 9, 9, 0}
(13)arr[10]=1,因此在第十九位开始修改1位为10,a[19] = 10,也就是
{0, 1, 1, 1, 2, 3, 3, 4, 4, 4, 5, 6, 7, 7, 7, 9, 9, 9, 9, 10}
(14)排序完成了,看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 计数排序
*/
public class CountSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 10; i++) {
nums.add(random.nextInt(10));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = countSort(nums);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List countSort(List nums) {
// 初始化以及获取数列中的最大值最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.size(); i++) {
if ((int)nums.get(i) > max) {
max = (int)nums.get(i);
}
if ((int)nums.get(i) < min) {
min = (int)nums.get(i);
}
}
// 计算最大值最小值的差值
int arrSize = max - min + 1;
// 根据差值初始化计数数组
int arr[] = new int[arrSize];
// 根据nums中的各个数值的重复度设定计数数组值
for (int i = 0; i < nums.size(); i++) {
int index = (int)nums.get(i) - min;
arr[index] ++;
}
// 输出计数数组的各项值
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "-");
}
System.out.println("");
// 根据计数数组的各项值重新排列nums
int index = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i]; j++) {
nums.set(index, min + i);
index ++;
}
}
return nums;
}
}
33.排序算法-桶排序
(1)桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。
(2)例如数列{21,42,38,14,34,35,9,24,16,40,29,48,33,17,30},分成五个桶,找出最大数48,最小数9,差值为39,分成五个桶的话,每个桶防范围39/5=7.8,向上取整为8,因此五个桶的范围分别是9-17,18-26,27-35,36-43,44-52的五个桶。
(3)根据各个桶范围分别将数列放入到五个桶中,如下:
桶1 14,9,16,17,
桶2 21,24,
桶3 34,35,29,33,30
桶4 42,38,40,
桶5 48
(5)分别对五个桶排序,得到如下:
桶1 9,14,16,17,
桶2 21,24,
桶3 29,30,33,34,35,
桶4 38,40,42,
桶5 48
(6)分别从桶1至桶5把所有值逐个取出修改原有数列,得到结果
{9,14,16,17,21,24,29,30,33,34,35,38,40,42,48}
(7)排序完成,上代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 桶排序
*/
public class BucketSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 100; i++) {
nums.add(random.nextInt(100));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = bucketSort(nums);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List bucketSort(List nums) {
// 初始化以及获取数列中的最大值最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.size(); i++) {
if ((int) nums.get(i) > max) {
max = (int) nums.get(i);
}
if ((int) nums.get(i) < min) {
min = (int) nums.get(i);
}
}
// 计算最大最小差值,并分成5组,计算出每组范围
int gap = (int) Math.ceil(Double.valueOf(max - min) / 5);
int[][] bucket = new int[5][nums.size()];
int[] bucketIndex = new int[5];
for (int i = 0; i < nums.size(); i++) {
int index = ((int) nums.get(i) - min) / gap;
if (index >= 5) {
System.out.println("====");
}
bucket[index][bucketIndex[index]++] = (int) nums.get(i);
}
// 分别对每个桶排序,咱们就直接冒泡排序了
for (int i = 0; i < 5; i++) {
bucket[i] = bubbleSort(bucket[i], bucketIndex[i]);
}
// 遍历上面的桶,逐个取出放入到新的list中
List newNums = new ArrayList();
for (int j = 0; j < bucket.length; j++) {
for (int k = 0; k < bucketIndex[j]; k++) {
newNums.add(bucket[j][k]);
}
}
// 将新list返回
return newNums;
}
public static int[] bubbleSort(int[] nums, int length) {
for (int i = 0; i < length; i++) {
for (int j = i + 1; j < length; j++) {
// 比较j元素与i元素大小,如果i元素大与j元素,则互换位置
if (nums[j] < nums[i]) {
int large = nums[i];
nums[i] = nums[j];
nums[j] = large;
}
}
}
return nums;
}
}
34.排序算法-基数排序
(1)基数排序,就是按照整数的个位、十位、百位等依次排列元素,局部最优排列最终可以获得全局最优。
(2)基数排序可以分为LSD和MSD两种,LSD就是从低位往高位排(个十百…),MSD是从高位往低位排(…百十个)。
(3)咱们通过LSD展示一下集体逻辑,下面这个数列{345,21,342,786,55,2,453,66,98,145,46,76,5,674}
(4)首先遍历这些数字,取得最大值786,根据最大值获取到这个数列中存在的个十百位,没有千位及以上。因此只需要分别按照个位,十位,百位遍历分别放入三次桶。
(5)先按照个位的各种值放入到10个桶中(因为个位数字只有0-9共计10中数字),放入结果如下。
桶0
桶1 21,
桶2 342,2,
桶3 453,
桶4 674,
桶5 345,55,145,5,
桶6 786,66,46,76,
桶7
桶8 98,
桶9
(6)分别从桶0至桶10,逐个取出,逐个修改数列为
{21,342,2,453,674,345,55,145,5,786,66,46,76,98}
(7)再按照十位的各种值放入到10个桶中(因为十位数字只有0-9共计10中数字),放入结果如下。
桶0 2,5,
桶1
桶2 21,
桶3
桶4 342,345,145,46,
桶5 453,55,
桶6 66,
桶7 674,76,
桶8 786,
桶9 98,
(8)分别从桶0至桶10,逐个取出,逐个修改数列为
{2,5,21,342,345,145,46,453,55,66,674,76,786,98}
(9)再按照百位的各种值放入到10个桶中(因为百位数字只有0-9共计10中数字),放入结果如下。
桶0 2,5,21,46,55,66,76,98,
桶1 145,
桶2
桶3 342,345,
桶4 453,
桶5
桶6 674,
桶7 786,
桶8
桶9
(10)分别从桶0至桶10,逐个取出,逐个修改数列为
{2,5,21,46,55,66,76,98,145,342,345,453,674,786}
(11)排序完成,接下来看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 基数排序
*/
public class RadixSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 100; i++) {
nums.add(random.nextInt(1000));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = radixSort(nums);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List radixSort(List nums) {
// 初始化以及获取数列中的最大值最小值
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.size(); i++) {
if ((int) nums.get(i) > max) {
max = (int) nums.get(i);
}
}
// 计算出该数字的个十百千万位,以便计算需要遍历几次
int cur = 1;
while(max / (int)Math.pow(10, cur)>0) {
cur++;
}
System.out.println(cur);
for (int i = 1; i <= cur; i++) {
int[][] bucket = new int[10][nums.size()];
int[] bucketIndex = new int[10];
// 分别计算,个位,十位,百位的数字
for (int j = 0; j < nums.size(); j++) {
int rest = (int)nums.get(j) / ((int)Math.pow(10, i-1)) % ((int)Math.pow(10, i));
// 例如969,在取十位的数字时,上面的公式得到的是96,因此需要下面的再次计算
rest = rest % 10;
// 存入到对应位数的数字的桶中
bucket[rest][bucketIndex[rest]++] = (int)nums.get(j);
}
// 遍历上面的桶,逐个取出放入到新的list中
List newNums = new ArrayList();
for (int j = 0; j < bucket.length; j++) {
for (int k = 0; k < bucketIndex[j]; k++) {
newNums.add(bucket[j][k]);
}
}
// 将新list覆盖回旧list中
nums = newNums;
}
return nums;
}
}
35.排序算法-堆排序
(1)堆排序是利用堆数据结构而设计的一种排序算法,堆排序是一种选择排序,其最坏,最好,平均时间复杂度均为O(nlogn),同时也是不稳定排序。
(2)咱们使用的是大顶堆或者小顶堆。
(3)堆是具有以下性质的完全二叉树,每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
升序采用大顶堆,降序采用小顶堆。
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
(4)实际上,堆排序的算法就是自底向上排序,遍历取出整棵树种最大的节点,放到堆顶,这样堆里面最大的值就在堆顶了,因此最大的元素已经排序完成,然后将堆顶元素与堆最后一个元素调换顺序,放到最后,这样对去掉最后一个元素后的新堆重新自底向上排序,遍历出最大的元素,放到堆顶,再次对该元素与新堆的最后一个元素调换顺序,再次去掉该元素生成新堆,一遍一遍的遍历,到最后,实际上就是从小打到排号顺序的元素了。
(5)虽然说是自底向上排序,但是堆实际上是一个棵树,因此只需要找到最后一个元素的所在子树的根节点即可,实际上也就是最后一个非叶子节点。因此也就是从后向前逐个遍历每一个非叶子结点,比较当前根节点与两个子节点比较大小,找到最大的元素,放在当前子树的根节点即可
(6)咱们手动走一波逻辑,例如数组{4,6,3,5,9},堆如下图:
4
/ \
6 3
/ \
5 9
(7)找到最后一个非叶子节点,也就是6所在元素位置,比较6和两个子节点大小,6>5,不需要替换,6<9,因此将6与9的位置替换,新数组{4,9,3,5,6},堆如下图:
4
/ \
9 3
/ \
5 6
(8)从后向前找下一个非叶子节点,也就是4所在元素位置,比较4和两个子节点大小,4<9,因此将4与9的位置替换,继续比较9>3,不需要替换,新数组{9,4,3,5,6},堆如下图:
9
/ \
4 3
/ \
5 6
(9)这时候需要特殊操作,将9(已经最大,有序了)与堆最后一个元素替换位置,新数组{6,4,3,5,9},堆如下图:
6
/ \
4 3
/ \
5 9
(10)此时再次针对数组前四个元素重新进行堆排序,数组{6,4,3,5,9(忽略该元素,已经有序)},堆如下图:
6
/ \
4 3
/
5
(11)找到最后一个非叶子节点,也就是4所在元素位置,比较4和子节点大小,4<5,因此将4与5的位置替换,新数组{6,5,3,4,9(忽略该元素,已经有序)},堆如下图:
6
/ \
5 3
/
4
(12)从后向前找下一个非叶子节点,也就是6所在元素位置,比较6和两个子节点大小,6>5,不需要替换,继续比较6>3,不需要替换,数组不变{6,5,3,4,9(忽略该元素,已经有序)},堆如下图:
6
/ \
5 3
/
4
(13)这时候需要特殊操作,将6(已经最大,有序了)与堆最后一个元素替换位置,新数组{4,5,3,6,9(忽略该元素,已经有序)},堆如下图:
4
/ \
5 3
/
6
(14)此时再次针对数组前三个元素重新进行堆排序,数组{4,5,3,6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
4
/ \
5 3
(15)找到最后一个非叶子节点,也就是4所在元素位置,比较4和两个子节点大小,4<5,因此将4与5的位置替换,再次比较5>3,不需要替换,新数组{5,4,3,6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
5
/ \
4 3
(16)这时候需要特殊操作,将5(已经最大,有序了)与堆最后一个元素替换位置,新数组{3,4,5,6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
3
/ \
4 5
(17)此时再次针对数组前两个元素重新进行堆排序,数组{3,4,5(忽略该元素,已经有序),6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
3
/
4
(18)找到最后一个非叶子节点,也就是3所在元素位置,比较3和子节点大小,3<4,因此将3与4的位置替换,新数组{4,3,5(忽略该元素,已经有序),6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
4
/
3
(19)这时候需要特殊操作,将4(已经最大,有序了)与堆最后一个元素替换位置,新数组{3,4,5(忽略该元素,已经有序),6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
3
/
4
(20)此时再次针对数组第一个元素重新进行堆排序,数组{3,4(忽略该元素,已经有序),5(忽略该元素,已经有序),6(忽略该元素,已经有序),9(忽略该元素,已经有序)},堆如下图:
3
(21)此时没有非叶子结点了,排序完成,此时数组已经有序,{3,4,5,6,9},排序完成。
(22)接下来看代码:
package com.xuesong.sort;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 堆排序
*/
public class HeapSort {
public static void main(String[] args) {
List nums = new ArrayList();
Random random = new Random();
for (int i = 0; i < 10; i++) {
nums.add(random.nextInt(10));
}
nums.forEach(s -> System.out.print(s + "-"));
System.out.println("");
nums = heapSort(nums);
nums.forEach(s -> System.out.print(s + "-"));
}
public static List heapSort(List nums) {
// 从右向左遍历
for (int i = nums.size() - 1; i >= 0; i--) {
int j = i;
// 取得最后一个非叶子节点
int lastNotLeafIndex = (j-1)/2;
while(lastNotLeafIndex >= 0 && i > 1) {
// 与左子节点比较,如果小于子节点,则替换顺序
if((int)nums.get(lastNotLeafIndex) < (int)nums.get(lastNotLeafIndex*2+1)) {
swap(nums, lastNotLeafIndex, lastNotLeafIndex*2+1);
}
// 与右子节点比较,如果小于子节点,则替换顺序
if((lastNotLeafIndex*2+2) <= i-1 && (int)nums.get(lastNotLeafIndex) < (int)nums.get(lastNotLeafIndex*2+2)){
swap(nums, lastNotLeafIndex, lastNotLeafIndex*2+2);
}
// 继续下一个非椰子节点
j--;
lastNotLeafIndex = (j-1)/2;
}
// 替换获取到的堆顶节点与最后一个节点顺序
swap(nums, i, 0);
}
return nums;
}
/**
* 交换位置
*
* @param nums
* @param left
* @param right
*/
public static void swap(List nums, int left, int right) {
int current = (int) nums.get(left);
nums.set(left, nums.get(right));
nums.set(right, current);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。