小灰(小白)的算法之旅
第一章 算法概述
1.1 算法和数据结构
- 算法(Algorithm):在数学领域用于解决某一类问题的公式和思想。如高斯算法:
n×(1+n)÷2
,可以用于解决1+2+3+……+(n-1)+n
的问题。在计算机领域用于解决特定的运算和逻辑问题。如:运算、查找、排序、最优决策。 - 数据结构(Data Structure):数据的组织、管理和存储格式。其使用目的是为了高效的访问和修改数据。如:线性结构(数组、链表)、树(二叉树、二叉堆)、图(多对多)、其他数据结构(哈希链表、跳表、位图等)等。
- 衡量算法的标准有两个时间复杂度和空间复杂度。
1.2 时间复杂度
基本操作次数T(n):线性T(n) = n;对数T(n) = log(n);常量T(n) = 1;多项式T(n) = n²+n。
渐进时间复杂度:官方定义所存在函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度。
时间复杂度推到原则:
-- 如果运行时间是常数量级,则用常数1表示;
-- 只保留时间函数中的最高阶项;
-- 如果最高阶项存在,则省去最高阶项前面的系数。大O表示法及时长对比:常量O(1) < 对数O(n) =O(logN) < 线性O(n) = n < 多项式O(n) = n²。
其他算法时间复杂度:O(nlogn)、O(n³)、O(mn)、O(2^n)、O(n!)
1.3 空间复杂度
- 空间计算公式:S(n) = O(f(n));常量空间O(1)、线性空间O(n)、二维空间O(n²)、递归空间O(n)
递归算法的空间复杂度度同深度成正比。
第二章 数据结构基础
2.1 数组
- 数组(Array)是有限个相同类型的变量所组成的有序集合,每一个变量被称为元素。
- 数组在内存中是顺序存储。
- 操作:读取和更新时间复杂度为O(1),插入和删除时间复杂度为O(n)。
- 优势和劣势:数组适合读操作多,写操作少的场景。
- 数组基本操作相关代码:
public class ArrayBaseOperation {
//数组定义
private int[] array = new int[]{3, 1, 2, 5, 4, 9, 7, 2};
//记录元素个数
private int size = array.length;
private void demo() {
//读取
System.out.println("原始数据:");
output();
//更新
array[2] = 10;
//插入
insert(8, 2);
System.out.println("插入后的数据:");
output();
//删除
delete(5);
System.out.println("删除后的数据:");
output();
}
private void output(){
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
//System.out.println(Arrays.toString(array));
}
/**
* 删除操作
* @param index 删除的位置
* @return
*/
public int delete(int index) {
checkIndexBounds(index);
int deletedElement = array[index];
for (int i = index; i < size; i++) {
array[i] = array[i + 1];
}
size--;
return deletedElement;
}
/**
* 插入操作
* @param element 插入的元素
* @param index 插入的位置
*/
public void insert(int element, int index) {
checkIndexBounds(index);
if (size >= array.length) {
//扩容
resize();
}
for (int i = size - 1; i >= index; i--) {
array[i + 1] = array[i];
}
array[index] = element;
size++;
}
private void checkIndexBounds(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出数组实际元素范围!");
}
}
/**
* 扩容
*/
private void resize() {
int[] arrayNew = new int[array.length * 2];
System.arraycopy(array, 0, arrayNew, 0, array.length);
array = arrayNew;
}
/**
* @return 获取元素个数
*/
public int getSize() {
return size;
}
public static void main(String[] args) {
ArrayBaseOperation operation = new ArrayBaseOperation();
operation.demo();
}
}
输出结果:
> Task :lib:ArrayBaseOperation.main()
原始数据:
3 1 2 5 4 9 7 2
插入后的数据:
3 1 8 10 5 4 9 7 2
删除后的数据:
3 1 8 10 5 9 7 2
BUILD SUCCESSFUL in 0s
2.2 链表
- 链表(Linked List)是一种在物理上非连续、非顺序的数据结构,由若干节点(node)所组成。
- 链表在内存中是随机存储。
- 链表分单向链表和双向链表。
- 操作:查找时间复杂度是O(n),更新、插入、删除时间复杂度是O(1)(不考虑查找过程)。
- 优势和劣势:与数组相反,适合读操作少,写操作多的场景。
- 链表相关操作代码:
public class LinkedListBaseOperation {
private Node head;//头节点
private Node last;//尾节点
private int size;//链表长度
public void demo() {
//插入数据
insert(3, 0);
// output();
insert(5, 1);
// output();
insert(7, 2);
// output();
insert(9, 0);
// output();
insert(4, 3);
// output();
insert(2, 3);
// output();
insert(1, 1);
System.out.println("插入数据后:");
//读取数据
output();
//删除数据
remove(0);
remove(3);
// remove(4);
System.out.println("删除数据后:");
output();
}
/**
* 数据输出
*/
public void output() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " ");
temp = temp.next;
}
System.out.println();
}
/**
* 数据删除
* @param index 指定删除节点位置
*/
public Node remove(int index) {
checkIndexBounds(index);
Node removedNode;
if (index == 0) {//删除头节点
removedNode = head;
head = head.next;
} else if (index == size - 1) {//删除尾节点
Node prevNode = get(index-1);
removedNode = prevNode.next;
prevNode.next = null;
last = prevNode;
} else {
Node prevNode = get(index-1);
Node nextNode = prevNode.next.next;
removedNode = prevNode.next;
prevNode.next = nextNode;
}
size--;
return removedNode;
}
/**
* @param data 插入的数据
* @param index 插入的位置
*/
public void insert(int data, int index) {
checkIndexBounds(index);
Node insertedNode = new Node(data);
if (size == 0) {
head = insertedNode;
last = insertedNode;
} else if (index == 0) {
insertedNode.next = head;
head = insertedNode;
} else {
Node prevNode = get(index - 1);//查找数据
Node nextNode = prevNode.next;
insertedNode.next = nextNode;
prevNode.next = insertedNode;
}
size++;
}
/**
* 数据查找
* @param index 节点位置
* @return 获取指定位置节点
*/
private Node get(int index) {
checkIndexBounds(index);
Node temp = head;
for (int i = 0; i < index; i++) {
temp = temp.next;
}
return temp;
}
private void checkIndexBounds(int index) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException("超出链表节点范围!");
}
}
/**
* 单向链表
*/
private static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
/**
* 双向链表
*/
private static class NodeTW {
int data;
NodeTW next;
NodeTW prev;
NodeTW(int data) {
this.data = data;
}
}
public static void main(String[] args) {
LinkedListBaseOperation operation = new LinkedListBaseOperation();
operation.demo();
}
}
输出结果:
> Task :lib:LinkedListBaseOperation.main()
插入数据后:
9 1 3 5 2 4 7
删除数据后:
1 3 5 4 7
BUILD SUCCESSFUL in 0s
2.3 栈和队列
- 栈是一种线性结构,它就像入口和出口是同一个口的圆桶容器,元素只能先进后出(First In Last Out,简称FILO)。最先进入的叫栈底,最后进入的叫栈顶。
- 栈的基本操作:入栈(push)新元素进入栈中成为新的栈顶;出栈(pop)栈顶出栈,前一个元素成为新的栈顶。时间复杂度都是O(1)。
- 队列是一种线性结构,它就像出口和入口是不同口的单行隧道,元素只能先入先出(First In First Out,简称FIFO)。出口端是队头,入口端是队尾。
- 操作:入队(enqueue)是把新的元素放入队列中并且成为新的队尾。出队(dequeue)就是把元素移除队列,并且后一个元素成为新的队头。
- 应用:栈可以用来做历史回溯,例如浏览器的回溯上一个页面。队列可以用来做历史顺序重演,例如公平锁队列等待,或者是网络爬虫模拟网站数据抓取。
实现方式:栈和队列都可以用数组或链表实现,但是队列中使用数组实现,称为循环队列,容量比数组长度小1。
循环队列关键算法:{(队头/队尾下标+1)%数组长度 }计算新队头/队尾下标,如果与队头下标相等,则队列已满。
双端队列:队头队尾都可以做入队或出队操作。
优先队列:非线性结构,按优先规则出队,如二叉堆。
- 循环队列相关代码
/**
* 循环队列
*/
public class QueueBaseOperation {
private int[] array;
private int front;
private int rear;
private int size;
public QueueBaseOperation(int capacity) {
array = new int[capacity];
}
public void demo() {
try {
enQueue(1); enQueue(3); enQueue(5); enQueue(7); enQueue(9); enQueue(7);
System.out.println("入队后:");
output();
deQueue();deQueue();deQueue();
System.out.println("出队后:");
output();
System.out.println("再入队:");
enQueue(1); enQueue(3); enQueue(5); enQueue(7);
output();
} catch (Exception e) {
e.printStackTrace();
}
}
public void output() {
for (int i = 0; i < size; i++) {
System.out.print(array[(front + i) % array.length] + " ");
}
System.out.println();
}
/**
* 入队
*
* @param element 入队元素
* @throws Exception 队列满了,抛出异常
*/
public void enQueue(int element) throws Exception {
if ((rear + 1) % array.length == front) {
throw new Exception("队列已满!");
}
array[rear] = element;
rear = (rear + 1) % array.length;
size++;
}
/**
* 出队
*
* @return 出队元素
* @throws Exception 队列空了,抛出异常
*/
public int deQueue() throws Exception {
if (rear == front) {
throw new Exception("队列已空!");
}
int deQueueElement = array[front];
front = (front + 1) % array.length;
size--;
return deQueueElement;
}
public static void main(String[] args) {
QueueBaseOperation operation = new QueueBaseOperation(10);
operation.demo();
}
}
输出结果:
> Task :lib:QueueBaseOperation.main()
入队后:
1 3 5 7 9 7
出队后:
7 9 7
7 9 7 1 3 5 7 9 7
BUILD SUCCESSFUL in 0s
2.4 散列表
- 散列表又叫哈希表(hash table),存储键(key)和值(value)的映射关系的集合(数组+链表/红黑树)。对于某一个key,散列表可以在接近O(1)的时间进行读写操作。散列表通过哈希函数实现Key和数组下标的转换,通过开放寻址法和链表法来解决哈希冲突。
哈希冲突:哈希算法在计算数组下标的时候可能会产生相同的下标,这就是所谓的冲突。
开放寻址法:产生哈希冲突,就向后移位寻找空位。如Java中ThreadLocal使用的就是此方法。
链表法:数组中每个元素可以作为链表头节点,冲突的元素向链表冲插入即可。
扩容(resize):当元素的数量>=负载因子×数组长度时,需要进行扩充哈希表容量,首先是生成新的数组,然后对原始数据中的元素重新Hash(rehash)计算,放入新的哈希表中。
第三章 树和二叉树
3.1 树
- 树(tree)是n(n>=0)个节点的有限集。当n=0时,称为空树。在任意一个非空树中,有如下特点:1.有且有一个特定的称为根的节点。2.当n>1时,其余节点可分为m(m>0)个互不相交的有限集,每个集合又是一个树,并称为根的子树。
- 二叉树:是树的特殊形式,每个节点最多又两个子节点。
- 满二叉树:一个二叉树的所有非叶子节点都存在左右孩子,并且所有叶子节点都在同一层级上。
- 完全二叉树:对一个有n个节点的二叉树,按层级顺序编号,则所有节点的编号为从1到n。如果这个树的所有节点和同样深度的满二叉树的编号为从1到n的节点位置相同,则这个二叉树为完全二叉树。
- 链式存储结构:数据变量、左孩子指针、右孩子指针。
- 数组存储:父节点parent、左孩子下标2×parent+1、有孩子下标2×parent+2。
- 查找应用:二叉查找树(binary search tree)
--如果左树不为空,则左子树所有节点的值均小于根节点的值。
--如果右树不为空,则右子树所有节点的值均大于根节点的值。
--左、右子树也都是二叉查找树。
对于节点分布相对均衡的二叉查找树来说,如果节点总数是n,那么搜索节点的时间复杂度就是O(logn),和树的深度一样。(类似二分查找) - 维持相对顺序应用:二叉排序树(binary sort tree)
二叉排序树即是二叉查找树,在插入元素时同样要满足左、右子树特点,但是有时候会导致元素分布不均衡的情况,需要通过自平衡解决,如红黑树、AVL树、树堆等。
3.2 二叉树的遍历
- 深度优先遍历
--前序遍历:根节点、左子树、右子树。
--中序遍历:左子树、根节点、右子树。
--后序遍历:左子树、右子树、根节点。
实现方式用递归或栈。 - 广度优先遍历
--层序遍历:从根节点到叶子节点逐层遍历。
实现方式用队列。 - 二叉树遍历相关代码:
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
public class BinaryTreeTraversal {
public void demo() {
LinkedList inputList = new LinkedList<>(Arrays.asList(3, 2, 9, null, null, 10, null, null, 8, null, 4));
TreeNode node = createBinaryTree(inputList);
System.out.println("前序遍历:");
preOrderTraversal(node);
System.out.println();
System.out.println("中序遍历:");
inOrderTraversal(node);
System.out.println();
System.out.println("后序遍历:");
postOrderTraversal(node);
System.out.println();
System.out.println("层序遍历:");
levelOrderTraversal(node);
}
/**
* 构建二叉树
* @param inputList 数据列表
* @return 根节点
*/
public TreeNode createBinaryTree(LinkedList inputList) {
TreeNode treeNode = null;
if (inputList != null && !inputList.isEmpty()) {
Integer data = inputList.removeFirst();
if (data != null) {
treeNode = new TreeNode(data);
treeNode.leftChild = createBinaryTree(inputList);
treeNode.rightChild = createBinaryTree(inputList);
}
}
return treeNode;
}
/**
* 前序遍历
* @param node 根节点
*/
public void preOrderTraversal(TreeNode node) {
if (node == null) return;
System.out.print(node.data + " ");
preOrderTraversal(node.leftChild);
preOrderTraversal(node.rightChild);
}
/**
* 中序遍历
* @param node 根节点
*/
public void inOrderTraversal(TreeNode node) {
if (node == null) return;
inOrderTraversal(node.leftChild);
System.out.print(node.data + " ");
inOrderTraversal(node.rightChild);
}
/**
* 后序遍历
* @param node 根节点
*/
public void postOrderTraversal(TreeNode node) {
if (node == null) return;
postOrderTraversal(node.leftChild);
postOrderTraversal(node.rightChild);
System.out.print(node.data + " ");
}
/**
* 层序遍历
* @param node 根节点
*/
public void levelOrderTraversal(TreeNode node) {
Queue queue = new LinkedList<>();
queue.offer(node);
while (!queue.isEmpty()) {
TreeNode treeNode = queue.poll();
System.out.print(treeNode.data + " ");
if (treeNode.leftChild != null) {
queue.offer(treeNode.leftChild);
}
if (treeNode.rightChild != null) {
queue.offer(treeNode.rightChild);
}
}
}
private static class TreeNode {
int data;
TreeNode leftChild;
TreeNode rightChild;
TreeNode(int data) {
this.data = data;
}
}
public static void main(String[] args) {
BinaryTreeTraversal traversal = new BinaryTreeTraversal();
traversal.demo();
}
}
输出内容:
> Task :lib:BinaryTreeTraversal.main()
前序遍历:
3 2 9 10 8 4
中序遍历:
9 2 10 3 8 4
后序遍历:
9 10 2 4 8 3
层序遍历:
3 2 8 9 10 4
BUILD SUCCESSFUL in 0s
树形图:
3
/ \
2 8
/ \ / \
9 10 null 4
/ \ / \
null null null null
构建二叉树的构建递归过程比较难理解,先左后右,null值控制左右节点递归回溯(如果列表中没有null值,则所有数据为左树子节点),数据列表顺序与前序遍历顺序一致
3.3 二叉堆
- 最大堆(大顶堆):任何一个父节点的值,都大于或等于它左、右孩子节点的值。
- 最小堆(小顶堆):任何一个父节点的值,都小于或等于它左、右孩子节点的值。
- 插入节点:插入位置是完全二叉树最后一个位置,然后与父节点对比做“上浮”操作。
- 删除节点:删除堆顶,将完全二叉树最后一个节点移位到堆顶,与左右孩子中最小或最大节点对比做“下沉”操作。
- 构建二叉堆:将一个无序的二叉树调整为一个二叉堆,本质是让所有非叶子节点依次下沉。(与左右孩子中最小或最大节点对比)
插入和删除时间复杂度是:O(logn);构建二叉堆时间复杂度是:O(n)。
- 二叉堆操作相关代码:
import java.util.Arrays;
public class BinaryTreeHeap {
public void demo() {
System.out.println("上浮前:");
int[] array = new int[]{1, 3, 2, 6, 5, 7, 8, 9, 10, 0};
System.out.println(Arrays.toString(array));
upAdjust(array);
System.out.println("上浮后:");
System.out.println(Arrays.toString(array));
System.out.println("无序数组:");
array = new int[]{7, 1, 3, 10, 5, 2, 8, 9, 6};
System.out.println(Arrays.toString(array));
buildHeap(array);
System.out.println("构建成二叉堆后:");
System.out.println(Arrays.toString(array));
}
/**
* 上浮(最小堆)
* @param array 待调整数组
*/
public void upAdjust(int[] array) {
int childIndex = array.length - 1;
int parentIndex = (childIndex - 1) / 2;
int temp = array[childIndex];
while (childIndex > 0 && temp < array[parentIndex]) {
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = (parentIndex - 1) / 2;
}
array[childIndex] = temp;
}
/**
* 下浮(最小堆)
* @param array 待调整数组
* @param parentIndex 父节点位置
*/
public void downAdjust(int[] array, int parentIndex) {
int temp = array[parentIndex];
int childIndex = parentIndex * 2 + 1;
int length = array.length;
while (childIndex < length) {
if (childIndex + 1 < length && array[childIndex + 1] < array[childIndex]) {
childIndex++;
}
if (temp <= array[childIndex]) {
break;
}
array[parentIndex] = array[childIndex];
parentIndex = childIndex;
childIndex = childIndex * 2 + 1;
}
array[parentIndex] = temp;
}
/**
* 将无序数组构建二叉堆(最小堆)
* @param array 无序的数组
*/
public void buildHeap(int[] array) {
for (int i = (array.length - 2) / 2; i >= 0; i--) {
downAdjust(array, i);
}
}
public static void main(String[] args) {
BinaryTreeHeap heap = new BinaryTreeHeap();
heap.demo();
}
}
输出结果:
> Task :lib:BinaryTreeHeap.main()
上浮前:
[1, 3, 2, 6, 5, 7, 8, 9, 10, 0]
上浮后:
[0, 1, 2, 6, 3, 7, 8, 9, 10, 5]
无序数组:
[7, 1, 3, 10, 5, 2, 8, 9, 6]
构建成二叉堆后:
[1, 5, 2, 6, 7, 3, 8, 9, 10]
BUILD SUCCESSFUL in 0s
二叉堆为顺序存储(数组),childLeft = parent2+1,childRight = parent2+2。
上面的demo都是以最小堆特点进行构建和排序。
3.4 优先队列
- 队列的特点是先进先出(FIFO),而优先队列,则是基于二叉堆的特点所实现的一种顺序队列。优先队列可以根据对比元素的大小值,进而判断元素的优先出队顺序,于元素的入队顺序无关。
- 优先队列的代码实现基本与二叉堆无异,只是在入队时需要进行动态扩容,出队时需要删除堆顶。
第四章 排序算法
4.1 引言
时间复杂度 | 排序算法名称 |
---|---|
O(n²) | 冒泡排序、选择排序、插入排序、希尔排序 |
O(nlogn) | 快速排序、归并排序、堆排序 |
线性 | 计数排序、桶排序、基数排序 |
希尔排序比较特殊,它的性能略优于O(n²),但又略低于O(nlogn)
- 稳定排序和不稳定排序:稳定排序能保证相同元素在排序前和排序后的相对顺序,不稳定排序则不能。
4.2 冒泡排序
- 冒泡排序(dubble sort),是一种基础的交换排序。
升序:把相邻的两个元素进行比较,当一个元素大于右侧相邻元素时,交换他们的位置;当一个元素小于或等于相邻元素时,位置不变。
冒泡排序属于稳定排序,时间复杂度为O(n²)。 - 鸡尾酒排序:由冒泡排序演化而来,如果说普通冒泡排序是单向排序,鸡尾酒则是双向排序。适用于大部分元素有序的情况。
- 代码实现:
package com.power.dapengeducation.lib;
import java.util.Arrays;
import java.util.Random;
public class BubbleSort {
private int[] arr = {7, 8, 9, 1, 2, 3, 6, 5, 4};
public void demo() {
// int len = 100000;
// arr = new int[len];
// Random random = new Random();
// for (int i = 0; i array[j + 1]) {
tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
isSorted = false;
}
}
if (isSorted) break;
isSorted = true;
//右半边左移
for (int j = array.length - i - 1; j > i; j--) {
if (array[j] < array[j - 1]) {
tmp = array[j];
array[j] = array[j - 1];
array[j - 1] = tmp;
isSorted = false;
}
}
if (isSorted) break;
}
}
/**
* 二次优化的冒泡排序(升序)
*
* @param array 待排序数组
*/
public static void optimalSortAsc(int[] array) {
int sortBorder = array.length - 1;//记录无序边界
int sortBorderTemp = 0;
for (int i = 0; i < array.length; i++) {
boolean isSorted = true;//有序标记
for (int j = 0; j < sortBorder; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
isSorted = false;
sortBorderTemp = j;
}
}
sortBorder = sortBorderTemp;
if (isSorted) {
break;
}
}
}
/**
* 无优化的冒泡排序(升序)
*
* @param array 待排序数组
*/
private static void baseSortAsc(int array[]) {
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - 1; j++) {
if (array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
}
}
}
}
public static void main(String[] args) {
BubbleSort sort = new BubbleSort();
sort.demo();
}
}
输出结果:
> Task :lib:BubbleSort.main()
排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
BUILD SUCCESSFUL in 0s
4.3 快速排序
- 快速排序也属于交换排序。快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到另一边,从而把数列拆解成两个部分。这种思路叫分治法。
- 平均时间复杂度O(nlogn)。
- 选择基准元素:随机。
- 元素交换:双边循环法和单边循环法。
- 相关代码实现:
import java.util.Arrays;
public class QuickSort {
public void demo() {
int[] arr = {4, 4, 6, 5, 3, 2, 8, 1};
System.out.println("排序前:" + Arrays.toString(arr));
// quickSortBilateral(arr, 0, arr.length - 1);
quickSortUnilateral(arr, 0, arr.length - 1);
System.out.println("排序后:" + Arrays.toString(arr));
}
/**
* 单边循环法快速排序
*
* @param arr 待排序数组
* @param startIndex 起始位置
* @param endIndex 结束位置
*/
public void quickSortUnilateral(int[] arr, int startIndex, int endIndex) {
if (startIndex >= endIndex) return;
int pivotIndex = partitionUnilateral(arr, startIndex, endIndex);
quickSortUnilateral(arr, startIndex, pivotIndex - 1);
quickSortUnilateral(arr, pivotIndex + 1, endIndex);
}
/**
* 单边循环法分治
*
* @param arr 待排序数组
* @param startIndex 起始位置
* @param endIndex 结束位置
* @return 基准位置
*/
private int partitionUnilateral(int[] arr, int startIndex, int endIndex) {
int pivot = arr[startIndex];
int mark = startIndex;
for (int i = startIndex + 1; i <= endIndex; i++) {
if (arr[i] < pivot) {
mark++;
int temp = arr[i];
arr[i] = arr[mark];
arr[mark] = temp;
}
}
arr[startIndex] = arr[mark];
arr[mark] = pivot;
return mark;
}
/**
* 双边循环快速排序
*
* @param arr 待排序数组
* @param startIndex 起始下标
* @param endIndex 结束下标
*/
public void quickSortBilateral(int[] arr, int startIndex, int endIndex) {
if (startIndex >= endIndex) return;
int pivotIndex = partitionBilateral(arr, startIndex, endIndex);
quickSortBilateral(arr, startIndex, pivotIndex - 1);
quickSortBilateral(arr, pivotIndex + 1, endIndex);
}
/**
* 双边循环分治
*
* @param arr 待交换数组
* @param startIndex 起始下标
* @param endIndex 结束下标
* @return
*/
public int partitionBilateral(int[] arr, int startIndex, int endIndex) {
int pivot = arr[startIndex];//也可以随机选择基准元素
int left = startIndex;
int right = endIndex;
while (left != right) {
while (right > left && arr[right] > pivot) {
right--;
}
while (left < right && arr[left] <= pivot) {
left++;
}
if (arr[left] > arr[right]) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
arr[startIndex] = arr[left];
arr[left] = pivot;
return left;
}
public static void main(String[] args) {
QuickSort sort = new QuickSort();
sort.demo();
}
}
输出结果:
> Task :lib:QuickSort.main()
排序前:[4, 4, 6, 5, 3, 2, 8, 1]
排序后:[1, 2, 3, 4, 4, 5, 6, 8]
BUILD SUCCESSFUL in 0s
以上排序代码是用递归方式实现,也可以用栈模拟递归实现排序算法,代码略微复杂,此处省略。
4.4 堆排序
- 堆排序同3.3节 二叉堆的上浮、下浮操作。
- 与快速排序对比:平均时间复杂度都是O(nlogn),并且都是不稳定排序。快速排序最坏时间复杂度是O(n²),而堆排序最坏时间复杂度稳定在O(nlogn)。空间复杂度:快速排序是O(logn),堆排序是O(1).
4.5 计数排序和桶排序
- 计数排序:对原始数据不做变更,先生成一个统计数组,统计数组长度为待排序数组的最大值(max value),利用统计数组下标与待排序数组元素对比,相同就在统计数组对应下标的元素+1,最后按照统计数组中记录的个数依次输出下标值(0跳过)的顺序,既是排序后的数据。
计数排序优化(稳定排序):统计数组长度为(max - min +1)。统计数组中元素值与前面所有元素值相加(变形),得出最终排序位置,然后对待排序数组进行倒序遍历,在统计数组查出对应元素的值并-1,便会得出待排序数组元素排序后的下标值。
计数排序适用于整数排序,不适用于最大值与最小值差距过大或者元素不是整数。
- 优化后的计数排序代码:
import java.util.Arrays;
public class CountSort {
public void demo() {
int[] arr = {95, 94, 91, 98, 99, 90, 99, 93, 91, 92};
System.out.println(Arrays.toString(countSort(arr)));
}
public int[] countSort(int[] arr) {
int max = arr[0];
int min = arr[0];
for (int i = 1; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
if (min > arr[i]) {
min = arr[i];
}
}
int d = max - min;
//创建统计数组并统计元素个数
int[] countArray = new int[d + 1];
for (int i = 0; i < arr.length; i++) {
countArray[arr[i] - min]++;
}
//变形处理
for (int i = 1; i < countArray.length; i++) {
countArray[i] += countArray[i - 1];
}
//根据统计数组得出排序后数组
int[] sortedArray = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
sortedArray[countArray[arr[i] - min] - 1] = arr[i];
countArray[arr[i] - min]--;
}
return sortedArray;
}
public static void main(String[] args) {
CountSort countSort = new CountSort();
countSort.demo();
}
}
输出结果:
:lib:CountSort.main()
[90, 91, 91, 92, 93, 94, 95, 98, 99, 99]
BUILD SUCCESSFUL in 2s
- 桶排序:桶排序需要创建若干个桶协助排序
可以与元素个数相同或其他
,每个桶代表一个区间范围区间跨度 = (max - min) /( 同数量-1)
,里面可以装载一个或多个元素,桶按照区间值大小顺序排列,再分别对每个桶中的元素进行排序排序算法自行选择
。
桶排序能解决计数排序的缺点,对整型或浮点型数据都支持,需要注意的是桶的数量控制。
- 桶排序相关代码:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
public class BucketSort {
public void demo() {
double[] arr = {4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09};
System.out.println(Arrays.toString(bucketSort(arr)));
}
public double[] bucketSort(double[] array) {
double max = array[0];
double min = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max) {
max = array[i];
}
if (array[i] < min) {
min = array[i];
}
}
//初始化桶
int bucketNum = array.length;
double d = max - min;
ArrayList> bucketList = new ArrayList<>(bucketNum);
for (int i = 0; i < bucketNum; i++) {
bucketList.add(new LinkedList());
}
//向桶中填充元素
for (int i = 0; i < array.length; i++) {
//找出桶的位置(有点难理解)
int index = (int) ((array[i] - min) * (bucketNum - 1) / d);
//放入桶中
bucketList.get(index).add(array[i]);
}
//排序
for (int i = 0; i < bucketList.size(); i++) {
Collections.sort(bucketList.get(i));
}
//输出
double[] sortedArray = new double[array.length];
int index = 0;
for (LinkedList list : bucketList) {
for (double element : list) {
sortedArray[index] = element;
index++;
}
}
return sortedArray;
}
public static void main(String[] args) {
BucketSort sort = new BucketSort();
sort.demo();
}
}
输出结果:
> Task :lib:BucketSort.main()
[0.0023, 2.123, 3.0, 4.12, 4.12, 6.421, 8.122, 10.09]
BUILD SUCCESSFUL in 0s
4.6 小结
排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定排序 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
鸡尾酒排序 | O(n²) | O(n²) | O(1) | 稳定 |
快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+m) | O(n+m) | O(m) | 稳定 |
桶排序 | O(n) | O(nlogn) | O(n) | 稳定 |
第五章 面试中的算法
5.1 判断链表有环及环长度
- 双指针追及法:一个指
p1
针做单步遍历p1.next
,另一个指针p2
做双步遍历p2.next.next
,如果有环,则一定会出现重合p1==p2
,环长度:D
头节点到入环节点距离,S1
入环节点到相遇节点距离,S2
相遇节点到入环节点距离。p2
走两步,所以行走距离D+S1+S2+S1
,p1
走一步,所以行走距离是D+S1
,重合时p2走的距离是p1的两倍,所以2(D+S1) = D+2S1+S2
=>D=S2
。当第一次重合时,挪动一个指针到head,且两个指针都做单步遍历,当再次相遇则是入环节点,由此可以得出D/S2的长度,所以环长度=链表长度-D
- 相关代码:
import com.ieugene.algorithmdemo.LinkedListBaseOperation.Node;
public class LinkedCycle {
private int nodeSize = 0;//demo
public void demo() {
Node node1 = new Node(5);
nodeSize++;
Node node2 = new Node(3);
nodeSize++;
Node node3 = new Node(7);
nodeSize++;
Node node4 = new Node(2);
nodeSize++;
Node node5 = new Node(6);
nodeSize++;
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = node2;
System.out.println("链表环长度:" + cycleLength(node1));
System.out.println("链表是否有环:" + hasCycle(node1));
}
public int cycleLength(Node head) {
Node head1 = head;
Node head2 = head;
while (head1.next != null && head2.next.next != null) {
head1 = head1.next;
head2 = head2.next.next;
if (head1 == head2) {
head2 = head;
break;
}
}
int len = 0;
while (head1.next != null && head1 != head2) {
head1 = head1.next;
head2 = head2.next;
len++;
}
return nodeSize - len;
}
/**
* 判断链表中是否有环
*
* @param head 链表头节点
* @return 有环返回true,否则是false
*/
public boolean hasCycle(Node head) {
Node head1 = head;
Node head2 = head;
while (head1.next != null && head2.next.next != null) {
head1 = head1.next;
head2 = head2.next.next;
if (head1 == head2) return true;
}
return false;
}
public static void main(String[] args) {
LinkedCycle cycle = new LinkedCycle();
cycle.demo();
}
}
输出结果:
链表环长度:4
链表是否有环:true
Process finished with exit code 0
5.2 最小栈
- 题要求:保证出栈、入栈、取最小值时间复杂度都是O(1)。
- 思路:用备用栈存储最小值。
- 相关代码:
import java.util.Stack;
public class MinStack {
private Stack mainStack = new Stack<>();
private Stack minStack = new Stack<>();
public void demo() {
push(4);
push(9);
push(7);
push(3);
push(8);
push(5);
System.out.println("最小值:" + getMin());
pop();
pop();
pop();
System.out.println("三次出栈后最小值:" + getMin());
}
public void push(int element) {
mainStack.push(element);
if (minStack.isEmpty() || minStack.peek() >= element) {
minStack.push(element);
}
}
public int pop() {
int element = mainStack.pop();
if (!minStack.isEmpty() && minStack.peek().equals(element)) {
minStack.pop();
}
return element;
}
public int getMin() {
return minStack.peek();
}
public static void main(String[] args) {
MinStack stack = new MinStack();
stack.demo();
}
}
输出结果:
最小值:3
三次出栈后最小值:4
Process finished with exit code 0
5.4 求最大公约数
- 如果数a能被数b整除,a就叫做b的倍数,b就叫做a的约数。最大公约数指的是两个或多个整数共同的最大约数,也叫最大公因数。
- 思路:
--辗转相除法(欧几里得算法),两个正整数a和b(a>b),它们的最大公约数等于a除以b的余数c和b之间的最大公约数。
--更相减损法(更相减损术),是出自《九章算术》的一种求最大公约数的算法,它原本是为约分而设计的,但它适用于任何需要求最大公约数的场合。
-- 对比:辗转相除法(欧几里得算法)是基于取模运算,当两个数较大时,性能较差;更相减损法(更相减损术)是基于减法运算得出结果,当两个数大小差距较大时,计算性能也会被降低。
--结论:两个算法结合应用,并在更相减损法(更相减损术)基础上使用移位运算。
《九章算术》是中国古代的数学专著,其中的“更相减损术”可以用来求两个数的最大公约数,即“可半者半之,不可半者,副置分母、子之数,以少减多,更相减损,求其等也。以等数约之。”
-翻译成现代语言如下:
第一步:任意给定两个正整数;判断它们是否都是偶数。若是,则用2约简;若不是则执行第二步。
第二步:以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止。
则第一步中约掉的若干个2与第二步中等数的乘积就是所求的最大公约数。
其中所说的“等数”,就是最大公约数。求“等数”的办法是“更相减损”法。所以更相减损法也叫等值算法。
移位运算:位运算相关知识
当a和b均为偶数时,gcd(a,b) = 2 × gcd(a/2, b/2) = gcd(a>>1, b>>1)<<1。
当a为偶数,b为奇数时,gcd(a,b) = gcd(a>>1, b)。
当a为奇数,b为偶数时,gcd(a,b) = gcd(a, b>>1)。
当a和b均为奇数时,做一次更相减损gcd(a,b) = gcd(a-b, b)。
- 相关代码
public class GreatestCommonDivisor {
public void demo() {
System.out.println(getGCD(25, 5));
System.out.println(getGCD(100, 80));
System.out.println(getGCD(27, 14));
//System.out.println(getGCD2(25, 5));
//System.out.println(getGCD2(100, 80));
//System.out.println(getGCD2(27, 14));
//System.out.println(getGCD3(25, 5));
//System.out.println(getGCD3(100, 80));
//System.out.println(getGCD3(27, 14));
}
public int getGCD(int a, int b) {
if (a == b) return a;
if ((a & 1) == 0 && (b & 1) == 0) {
return getGCD(a >> 1, b >> 1) << 1;
} else if ((a & 1) == 0 && (b & 1) != 0) {
return getGCD(a >> 1, b);
} else if ((a & 1) != 0 && (b & 1) == 0) {
return getGCD(a, b >> 1);
} else {
int big = Math.max(a, b);
int small = Math.min(a, b);
return getGCD(big - small, small);
}
}
/**
* 辗转相除法
*
* @param a 正整数
* @param b 正整数
* @return 最大公约数
*/
public int getGCD2(int a, int b) {
int big = Math.max(a, b);
int small = Math.min(a, b);
if (big % small == 0) return small;
return getGCD2(big % small, small);
}
/**
* 更相减损术
*
* @param a 正整数
* @param b 正整数
* @return 最大公约数
*/
public int getGCD3(int a, int b) {
if (a == b) return a;
int big = Math.max(a, b);
int small = Math.min(a, b);
return getGCD3(big - small, small);
}
public static void main(String[] args) {
GreatestCommonDivisor divisor = new GreatestCommonDivisor();
divisor.demo();
}
}
输出结果:
5
20
1
Process finished with exit code 0
5.5 求无序数列排序后相邻元素最大差
- 思路:利用桶排序原理,将无序数列放置到各自桶中,同时得出每个桶中的最大值和最小值,然后遍历每个桶,用前面的桶最大值与后面桶最小值做差值计算,遍历完成边能得出最大差值。
5.6 用栈实现队列
- 思路:用两个栈存储队列元素,栈A模拟入栈操作,栈B模拟出栈操作。出栈操作之前如果栈B空了,需要将栈A中的元素导入栈B中。
- 相关代码:
import java.util.Stack;
public class StackSimulateQueue {
Stack pushStack = new Stack<>();
Stack popStack = new Stack<>();
public void demo() {
enqueue(1);
enqueue(2);
enqueue(3);
System.out.println("第一次出队:" + dequeue());
System.out.println("第二次出队:" + dequeue());
enqueue(4);
System.out.println("第三次出队:" + dequeue());
System.out.println("第四次出队:" + dequeue());
}
//入队
public void enqueue(int element) {
pushStack.push(element);
}
//出队
public Integer dequeue() {
if (popStack.isEmpty()) {
if (pushStack.isEmpty()) {
return null;
}
transfer();
}
return popStack.pop();
}
private void transfer() {
while (!pushStack.isEmpty()) {
popStack.push(pushStack.pop());
}
}
public static void main(String[] args) {
StackSimulateQueue queue = new StackSimulateQueue();
queue.demo();
}
}
输出结果:
第一次出队:1
第二次出队:2
第三次出队:3
第四次出队:4
Process finished with exit code 0
5.7 寻找全排列的下一个数
- 题意:整数中全部数组重新排列,找出一个仅大于原数的数列。例如给出数字12345,全排列的下一个数是12354。
- 思路:字典序算法
--从后向前查看逆序区,找到逆序区域的前一位,也就是数字置换的边界。
--从逆序区域的前一位和逆序区与中大于它的最小的数字交换位置。
--把原来的逆序区域转为顺序状态。 - 相关代码:
import java.util.Arrays;
public class FindNearestNumber {
public void demo() {
// int[] numbers = {1, 2, 3, 4, 5};
// int[] numbers = {1, 2, 3, 5, 4};
int[] numbers = {1, 2, 3, 5, 4, 7, 9};
System.out.println(Arrays.toString(findNearestNumber(numbers)));
}
public int[] findNearestNumber(int[] numbers) {
//从后向前查找逆序边界
int index = findTransferPoint(numbers);
//复制并入参,避免直接修改入参
int[] numbersCopy = Arrays.copyOf(numbers, numbers.length);
//如果边界值是0,说明当前是最小值
if (index == 0) return reverseLast(numbersCopy);
//把逆序区域的前一位和逆序区域中刚刚大于它的数字交换位置
exchangeHead(numbersCopy, index);
//把原来逆序区转为顺序
reverse(numbersCopy, index);
return numbersCopy;
}
private int[] reverseLast(int[] numbers) {
int len = numbers.length;
int temp = numbers[len - 1];
numbers[len - 1] = numbers[len - 2];
numbers[len - 2] = temp;
return numbers;
}
private int findTransferPoint(int[] numbers) {
for (int i = numbers.length - 1; i > 0; i--) {
if (numbers[i - 1] > numbers[i]) {
return i - 1;
}
}
return 0;
}
private int[] exchangeHead(int[] number, int index) {
int head = number[index - 1];
int nearestIndex = 0;
int min = Integer.MAX_VALUE;
for (int i = number.length - 1; i >= index; i--) {
if (head < number[i] && (number[i] - head) < min) {
nearestIndex = i;
min = number[i] - head;
}
}
number[index - 1] = number[nearestIndex];
number[nearestIndex] = head;
return number;
}
private int[] reverse(int[] number, int index) {
for (int i = index; i < number.length; i++) {
boolean isSorted = true;
for (int j = index; j < number.length - 1; j++) {
if (number[j] > number[j + 1]) {
int temp = number[j];
number[j] = number[j + 1];
number[j + 1] = temp;
isSorted = false;
}
}
if (isSorted) break;
}
return number;
}
public static void main(String[] args) {
FindNearestNumber nearestNumber = new FindNearestNumber();
nearestNumber.demo();
}
}
输出结果:
[1, 2, 4, 3, 5, 7, 9]
Process finished with exit code 0
5.8 删去k个数字后的最小值
- 题意:从一个整数中删去k个数字,使得新整数的值尽量小。例如:1593212,删除后是1212。
- 思路:贪心算法,依次求得局部最优解,最终得到全局最优解。
--局部最优解:完全正序数列12345
的最大值是它的逆序数列54321
,反过来依然成立,由此可以得出逆序数列边界删除,便是局部最优解。 - 相关代码:
public class RemoveDigits {
public void demo() {
System.out.println(removeDigits("1593212", 3));
System.out.println(removeDigits("30200", 1));
System.out.println(removeDigits("10", 2));
System.out.println(removeDigits("541270936", 3));
}
public String removeDigits(String num, int k) {
//创建一个栈用于接收所有数字
char[] stack = new char[num.length()];
int top = 0;
for (int i = 0; i < num.length(); i++) {
//遍历当前数字
char c = num.charAt(i);
//当栈顶数字大于当前遍历的数字,栈顶出栈,新数字入栈
while (top > 0 && stack[top - 1] > c && k > 0) {
k -= 1;
top -= 1;
}
stack[top++] = c;
}
//找出栈底开始都是0的位置
int offset = 0;
int newLen = num.length() - k;
while (offset < newLen && stack[offset] == '0') {
offset++;
}
return offset == newLen ? "0" : new String(stack, offset, newLen - offset);
}
public static void main(String[] args) {
new RemoveDigits().demo();
}
}
输出结果:
1212
200
0
120936
Process finished with exit code 0
5.9 大整数相加
- 题意:两个长度超过long类型的大整数相加,求结果。
- 思路:拆分数据,拆分长度为可以直接计算的长度即可,例如int类型数据是10位,为了防止溢出,可以拆分到9位即可。
- 相关代码
import java.util.Arrays;
public class BigNumberSum {
final int MAX_LENGTH = 9;
final int CARRY_VALUE = 1000000000;
public void demo() {
System.out.println(bigNumberSum("987654321987654321987", "222234567891"));
}
public String bigNumberSum(String bigNumberA, String bigNumberB) {
//拆分大数字
int[] arrayA = splitBigNumber(bigNumberA);
int[] arrayB = splitBigNumber(bigNumberB);
//创建与最大数字等长的数组,用于接收计算结果
int[] arraySum = new int[Math.max(arrayA.length, arrayB.length)];
System.out.println("array A: " + Arrays.toString(arrayA));
System.out.println("array B: " + Arrays.toString(arrayB));
int minLen = Math.min(arrayA.length, arrayB.length);
//判断是否需要进位
boolean isCarry = false;
for (int i = 0; i < minLen; i++) {
arraySum[i] = arrayA[i] + arrayB[i];
if (isCarry) {
arraySum[i] += 1;
isCarry = false;
}
if (arraySum[i] >= CARRY_VALUE) {
arraySum[i] -= CARRY_VALUE;
isCarry = true;
}
}
for (int i = minLen; i < arraySum.length; i++) {
if (arrayA.length > arrayB.length) {
arraySum[i] = arrayA[i];
} else {
arraySum[i] = arrayB[i];
}
if (isCarry) {
arraySum[i] += 1;
isCarry = false;
}
}
StringBuilder builder = new StringBuilder();
for (int i = arraySum.length - 1; i >= 0; i--) {
if (arraySum[i] == 0) {
builder.append("000000000");
} else {
builder.append(arraySum[i]);
}
}
return builder.toString();
}
private int[] splitBigNumber(String number) {
int length = getArrayLength(number);
int[] array = new int[length];
int end = number.length();
int start;
for (int i = 0; i < length; i++) {
start = Math.max(end - MAX_LENGTH, 0);
array[i] = Integer.parseInt(number.substring(start, end));
end = start;
}
return array;
}
private int getArrayLength(String number) {
int len = 1;
if (number.length() > MAX_LENGTH) {
len = number.length() / MAX_LENGTH;
if (number.length() % MAX_LENGTH != 0)
len++;
}
return len;
}
public static void main(String[] args) {
new BigNumberSum().demo();
}
}
输出结果:
array A: [654321987, 654321987, 987]
array B: [234567891, 222]
987654322209888889878
Process finished with exit code 0
5.10 求解金矿问题
- 题:有五个金矿,每个金矿含金量不同,每个金矿要求的开采开采人数不同,不允许开采一部分,要么都开采,要不都不采。
- 思路:这个问题属于动态规划类问题,类似与著名的“背包问题”,记得《算法图解》中就是用的背包问题进行讲解的。
--动态规划要点:确定全局最优解和最优子结构之间的关系,以及问题的边界。
--动态规划核心:自底向上求解。
--这个关系用数学公式表达:状态转移方程式。
//n为金矿数量
//w为工人数量
//g[]为金矿含金量数组
//p[]金矿开采所需人数数组
F(n,w) = 0 (n=0或w=0) //边界情况
F(n,w) = F(n-1,w)(n>=1,w=1,w>=p[n-1]) //常规情况,具有两种最优子结构(当前金矿开采或不开采)
- 相关代码
public class BestGoldMining {
public void demo() {
int w = 10;
int[] p = {5, 5, 3, 4, 3};
int[] g = {400, 500, 200, 300, 350};
System.out.println("最优收益:" + getBestGoldMining(w, p, g));
}
/**
* 开采金矿最优收益
*
* @param w 总人数
* @param p 金矿所需人数
* @param g 金矿含量
* @return
*/
public int getBestGoldMining(int w, int[] p, int[] g) {
//创建当前结果
int[] results = new int[w + 1];
//填充一维数组
for (int i = 1; i <= g.length; i++) {
for (int j = w; j >= 1; j--) {
if (j >= p[i - 1]) {
results[j] = Math.max(results[j], results[j - p[i - 1]] + g[i - 1]);
}
}
}
//返回最后一个格子的值
return results[w];
}
public static void main(String[] args) {
new BestGoldMining().demo();
}
}
输出结果:
最优收益:900
Process finished with exit code 0
5.11 寻找缺失的整数
- 题1:一个无序数组里有99个不重复的正整数,范围是1~100,唯独缺少范围内的一个整数,找出这个缺失的整数。
- 解1:1~100的和减去无序数组所有元素的和,差值便是缺失的整数。
- 题2:一个无序整数数组里有若干个正整数,范围是1~100,其中99个数出现偶数次,只有一个整数出现奇数次,找出出现奇数次的整数。
- 解2:利用异或运算(XOR),同位得0,不同位得1。将所有元素依次做异或运算,出现偶数次的元素相互抵消,最后只剩下出现奇数次的元素。
- 题3:如果题2中有两个出现奇数次的元素,请找出。
- 解3:利用分治法。假设两个数是A和B,如果所有元素依次做异或运算,就相当于A和B做异或运算,根据异或运算规则可以得出,A和B的二进制一定会有一个不同位,一个是1,一个是0,然后根据这个二进制数的特点进行分类,倒数第二位为1的归为一类,倒数第二位为0的归为一类。这样问题就回归到问题2,按照问题2的解法便可以找出A和B。
- 相关代码:
import java.util.Arrays;
public class FindLostNumber {
public void demo() {
int[] array = {4, 1, 2, 2, 5, 1, 4, 3};
System.out.println("两个数是:" + Arrays.toString(findLostNumber(array)));
}
public int[] findLostNumber(int[] array) {
//存储结果
int[] reslut = new int[2];
//第一次做整体异或运算
int xorResult = 0;
for (int i = 0; i < array.length; i++) {
xorResult ^= array[i];
}
if (xorResult == 0) return null;//等于0说明输入数据不符合题要求
//确定两个整数的不同位,以此来做分组
int separator = 1;
while (0 == (xorResult & separator)) {
separator <<= 1;
}
//第二次分组,进行异或运算
for (int i = 0; i < array.length; i++) {
if (0 == (array[i] & separator)) {
reslut[0] ^= array[i];
} else {
reslut[1] ^= array[i];
}
}
return reslut;
}
public static void main(String[] args) {
new FindLostNumber().demo();
}
}
输出结果:
两个数是:[5, 3]
Process finished with exit code 0
第六章 算法的实际应用
6.1 Bitmap的巧用
- Bitmap算法又称位图算法。低内存占用,高性能位运算是它的优势。缺点是最大最小值差距不能太大。
- 场景:用户标签分类查找
- Bitmap算法参考:JDK中BitSet
- Bitmap读写操作相关代码:
public class Bitmap {
//每一个word是一个long类型元素,对应一个64位二进制数据
private long[] words;
//Bitmap的位数大小
private int size;
public Bitmap(int size) {
this.size = size;
this.words = new long[(getWordIndex(size - 1)) + 1];
}
/**
* 判单Bitmap某一位状态
*
* @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
* @return
*/
public boolean getBit(int bitIndex) {
checkBounds(bitIndex);
int wordIndex = getWordIndex(bitIndex);
return (words[wordIndex] & (1L << bitIndex)) != 0;
}
private void checkBounds(int index) {
if (index < 0 || index > this.size - 1) {
throw new IndexOutOfBoundsException("超过Bitmap的有效范围");
}
}
/**
* 把Bitmap的某一位设置为true
*
* @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
*/
public void setBit(int bitIndex) {
checkBounds(bitIndex);
int wordIndex = getWordIndex(bitIndex);
words[wordIndex] |= (1L << bitIndex);
}
/**
* 定位Bitmap某一位所对应的word
*
* @param bitIndex 位图的第bitIndex位(bitIndex=0表示Bitmap左数第一位)
* @return
*/
private int getWordIndex(int bitIndex) {
return bitIndex >> 6;
}
public static void main(String[] args) {
Bitmap bitmap = new Bitmap(128);
bitmap.setBit(126);
bitmap.setBit(75);
System.out.println(bitmap.getBit(126));
System.out.println(bitmap.getBit(78));
}
}
输出结果:
true
false
Process finished with exit code 0
6.2 LRU算法应用
- LRU全称Least Recently Used,最近最少使用。
- 使用场景:长期不被使用,使用机率不大的数据,当内存达到一定阀值,就会清理电最少被使用的数据。
- 原理:内部使用哈希链表数据结构,使得哈希表中无序的数据存在前驱、后继的排列顺序关系。每次数据的读写都会重新排列链表结构。Java中LinkedHashMap对哈希链表有了很好的实现。
- 相关代码:
import com.ieugene.algorithmdemo.LinkedListBaseOperation.NodeTW;
import java.util.HashMap;
public class LRUCache {
private NodeTW head;
private NodeTW end;
//存储上限
private final int limit;
private final HashMap hashMap;
public LRUCache(int limit) {
this.limit = limit;
hashMap = new HashMap<>();
}
public Integer get(String key) {
NodeTW node = hashMap.get(key);
if (node == null) return null;
refreshNode(node);
return node.data;
}
public void put(String key, int value) {
NodeTW node = hashMap.get(key);
if (node == null) {
//key不存在,插入数据
if (hashMap.size() >= limit) {
String oldKey = removeNode(head);
hashMap.remove(oldKey);
}
node = new NodeTW(value);
node.key = key;
addNode(node);
hashMap.put(key, node);
} else {
//key存在,刷新数据
node.data = value;
refreshNode(node);
}
}
public void remove(String key) {
NodeTW node = hashMap.get(key);
removeNode(node);
hashMap.remove(key);
}
/**
* 刷新被访问的节点
*
* @param node 被访问的节点
*/
private void refreshNode(NodeTW node) {
//尾节点不需要重排
if (node == end) {
return;
}
//先删除
removeNode(node);
//在重新添加
addNode(node);
}
/**
* 删除节点
*
* @param node 被删除的节点
* @return 被删除的节点的key
*/
private String removeNode(NodeTW node) {
//只有一个节点
if (node == head && node == end) {
head = null;
end = null;
} else if (node == end) {
end = end.prev;
end.next = null;
} else if (node == head) {
head = head.next;
head.prev = null;
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
}
return node.key;
}
/**
* 插入节点
*
* @param node 被插入的节点
*/
private void addNode(NodeTW node) {
if (end != null) {
end.next = node;
node.prev = end;
node.next = null;
}
end = node;
if (head == null) {
head = node;
}
}
public static void main(String[] args) {
LRUCache lruCache = new LRUCache(5);
lruCache.put("001", 1);
lruCache.put("002", 1);
lruCache.put("003", 1);
lruCache.put("004", 1);
lruCache.put("005", 1);
lruCache.get("002");
lruCache.put("004", 2);
lruCache.put("006", 6);
System.out.println(lruCache.get("001"));
System.out.println(lruCache.get("006"));
}
}
输出结果:
null
6
Process finished with exit code 0
6.3 A星寻路算法
- A * search algorithm
- 原理:
--两个集合:OpenList:可到达的格子、CloseList:已到达的格子。
--一个公式:F = G + H
--步骤:1.从起点开始放入OpenList中,计算OpenList中F值最小的格子作为下一步即将到达的格子,然后从OpenList移除,添加到CloseList,表示已经检查过。2.找出上下左右所有可能到达的格子,看他们是否在两个列表中,如果不在则加入到OpenList中,并计算响应的G、H、F值,并把当前的格子作为他们的“父节点”。以此类推,重复迭代这个过程,如果OpenList中存在目标格子,每一个父节点和目标格子所经历的路径即是最短路径。
每一个格子有三个属性,左下G:从起点走到当前格子的成本,也就是已经花费了多少步;右下H:在不考虑障碍的情况下,从当前格子走到目标格子的距离,也就是离目标还有多远;左上F:G和H的综合评估,也就是从起点到达目标格子的总步数。
以估值高低来决定搜索优先次序的方法被称为启发式搜索。
- 相关代码:
import java.util.ArrayList;
import java.util.List;
public class AStartSearch {
private static final int[][] MAZE = {
{0, 0, 0, 0, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 0}
};
public Grid aStartSearch(Grid start, Grid end) {
ArrayList openList = new ArrayList<>();
ArrayList closeList = new ArrayList<>();
openList.add(start);
while (openList.size() > 0) {
Grid currentGrid = findMinGrid(openList);
openList.remove(currentGrid);
closeList.add(currentGrid);
List neighbors = findNeighbors(currentGrid, openList, closeList);
for (Grid grid : neighbors) {
if (!openList.contains(grid)) {
grid.initGrid(currentGrid, end);
openList.add(grid);
}
}
for (Grid grid : openList) {
if ((grid.x == end.x) && grid.y == end.y) {
return grid;
}
}
}
return null;
}
private Grid findMinGrid(ArrayList openList) {
Grid tempGrid = openList.get(0);
for (Grid grid : openList) {
if (grid.f < tempGrid.f) {
tempGrid = grid;
}
}
return tempGrid;
}
private ArrayList findNeighbors(Grid grid, List openList, List closeList) {
ArrayList gridList = new ArrayList<>();
if (isValidGrid(grid.x, grid.y - 1, openList, closeList)) {
gridList.add(new Grid(grid.x, grid.y - 1));
}
if (isValidGrid(grid.x, grid.y + 1, openList, closeList)) {
gridList.add(new Grid(grid.x, grid.y + 1));
}
if (isValidGrid(grid.x - 1, grid.y, openList, closeList)) {
gridList.add(new Grid(grid.x - 1, grid.y));
}
if (isValidGrid(grid.x + 1, grid.y, openList, closeList)) {
gridList.add(new Grid(grid.x + 1, grid.y));
}
return gridList;
}
private boolean isValidGrid(int x, int y, List openList, List closeList) {
if (x < 0 || x >= MAZE.length || y < 0 || y >= MAZE[0].length) {
return false;
}
//障碍物
if (MAZE[x][y] == 1) {
return false;
}
if (containGrid(openList, x, y)) {
return false;
}
if (containGrid(closeList, x, y)) {
return false;
}
return true;
}
private boolean containGrid(List grids, int x, int y) {
for (Grid n : grids) {
if ((n.x == x) && (n.y == y)) {
return true;
}
}
return false;
}
private static class Grid {
int x, y;
int f, g, h;
Grid parent;
public Grid(int x, int y) {
this.x = x;
this.y = y;
}
public void initGrid(Grid parent, Grid end) {
this.parent = parent;
if (parent != null) {
this.g = parent.g + 1;
} else {
this.g = 1;
}
this.h = Math.abs(this.x - end.x) + Math.abs(this.y - end.y);
this.f = this.g + this.h;
}
}
public static void main(String[] args) {
Grid startGrid = new Grid(2, 1);
Grid endGrid = new Grid(2, 5);
AStartSearch startSearch = new AStartSearch();
Grid result = startSearch.aStartSearch(startGrid, endGrid);
ArrayList path = new ArrayList<>();
while (result != null) {
path.add(new Grid(result.x, result.y));
result = result.parent;
}
for (int i = 0; i < MAZE.length; i++) {
for (int j = 0; j < MAZE[0].length; j++) {
if (startSearch.containGrid(path, i, j)) {
System.out.print("*, ");
} else {
System.out.print(MAZE[i][j] + ", ");
}
}
System.out.println();
}
}
}
输出结果:
0, 0, *, *, *, *, 0,
0, 0, *, 1, 0, *, 0,
0, *, *, 1, 0, *, 0,
0, 0, 0, 1, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0,
Process finished with exit code 0
6.4 红包算法
- 场景:随机拆分红包,数额差距尽量缩小。
- 算法:
--二倍均值法,公式:每次抢到的金额 = 随机区间[0.01, m/n * 2 - 0.01]元
,m为剩余金额,n为剩余人数。
--线段切割法,n个人抢,随机计算出n-1个切割点,随机范围区间是[1, m - 1],注意切割点重复和算法复杂度。 - 二倍均值法代码:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class DivideRedPackage {
public void demo() {
List amountList = divideRedPackage(1000, 10);
for (Integer amount : amountList) {
System.out.println("抢到的金额:" + new BigDecimal(amount).divide(new BigDecimal(100)));
}
}
public List divideRedPackage(Integer totalAmount, Integer totalPeopleNum) {
List amountList = new ArrayList<>();
Integer restAmount = totalAmount;
Integer restPeopleNum = totalPeopleNum;
Random random = new Random();
for (int i = 0; i < totalPeopleNum - 1; i++) {
int amount = random.nextInt(restAmount / restPeopleNum * 2 - 2) + 1;
restAmount -= amount;
restPeopleNum--;
amountList.add(amount);
}
amountList.add(restAmount);
return amountList;
}
public static void main(String[] args) {
new DivideRedPackage().demo();
}
}
输出结果:
抢到的金额:0.98
抢到的金额:1.5
抢到的金额:0.58
抢到的金额:0.52
抢到的金额:0.15
抢到的金额:1.55
抢到的金额:0.69
抢到的金额:2.09
抢到的金额:0.68
抢到的金额:1.26
Process finished with exit code 0
总结
总体来说还是通俗易懂的,但还是要吐槽一下勘误略多,缺乏严谨,还有个别地方不统一,例如最后面红包问题描述,单位就不统一,一会儿是元一会儿是分,对读者来说不是很友好。