一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
常见的时间复杂度有:常数阶O(1), 对数[logarithmic]阶O(log2n), 线性阶O(n), 线性对数阶O(nlog2n), 平方阶O(n2), 立方阶O(n3) ,k次方阶O(nk), 指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
算法时间复杂度由小到大依次为:Ο(1) < Ο(log2n)[二分] < Ο(n) < Ο(nlog2n) < Ο(n2) < Ο(n3) < … < Ο(2n) < Ο(n!)
求解算法的时间复杂度的具体步骤是:
空间复杂度比较常用的有:O(1)、O(n)、O(n²)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
如果新建一个数组,这个数据占用的大小为n,虽然有循环,但没有再分配新的空间,因此,空间复杂度主要看数组大小即可,即 S(n) = O(n)
线性:数组(Array)、链表(LinkedList)、栈(Stack)、队列(Queue)
非线形:多维数组、树结构、图结构
稀疏数组
场景:解压缩
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存。
处理方法是:
第一行记录数组一共有几行几列和不同值的个数;而后记录每行不同值的位置与实际值
环形队列
通过取模的方式实现。尾索引的下一个为头索引时表示队列满
双向链表
prev
,next
单向环形链表
(约瑟夫环)
先创建一个节点,让first指向自己,形成环形;后面每创建一个新节点,就将其加入到链表中(first.next=new;new.next=first
)。
栈
先进后出结构,入栈push(top++),出栈pop(top–) [poll取空不报错]
内排序:所有排序操作都在内存完成
外排序:由于数据太大,需要把数据放在磁盘,通过磁盘和内存的数据传输才能进行
重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。
public void maopao() {
int[] arr = {0, -1, 4, -2, 9, 5};
int temp;
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) { //趟数是len-1-i
if (arr[j + 1] < arr[j]) {
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
System.out.println(Arrays.toString(arr));
}
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
public void quickSort() {
int[] arr = {0, -1, 4, -2, 9, 5, -3, 4, 8, 7};
sort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private void sort(int[] arr, int left, int right) {
int l = left; //左下标
int r = right; //右下标
int pivot = arr[(left + right) / 2]; //中轴值
int temp = 0;
while (l < r) {
while (arr[l] < pivot) { //在pivot左边找
l += 1;
}
while (arr[r] > pivot) { //在pivot右边找
r -= 1;
}
if (l >= r) {
break;
}
//交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
if (arr[l] == pivot) { //前移
r -= 1;
}
if (arr[r] == pivot) { //后移
l += 1;
}
}
if (l == r) {
l += 1;
r -= 1;
}
if (left < r) { //向左递归
sort(arr, left, r);
}
if (right > l) { //向右递归
sort(arr, l, right);
}
}
首先在未排序的序列中找到最小(大)元素,存放到排序序列的起始位置;再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
public void chooseSort() {
int[] arr = {0, -1, 4, -2, 9, 5};
int mini, temp;
for (int i = 0; i < arr.length - 1; i++) {
mini = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[mini]) { //找到最小(或最大)
mini = j;
}
}
temp = arr[i];
arr[i] = arr[mini];
arr[mini] = temp;
}
System.out.println(Arrays.toString(arr));
}
也是一种 选择排序 ,也是一种 完全二叉树 :每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆;小于等于称为小顶堆。
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
public void insertSort() {
int[] arr = {0, -1, 4, -2, 9, 5};
int lasti, curr;
for (int i = 1; i < arr.length; i++) {
lasti = i - 1; //上一个索引
curr = arr[i];
while (lasti >= 0 && arr[lasti] > curr) {
arr[lasti + 1] = arr[lasti];
lasti--;
}
arr[lasti + 1] = curr;
}
System.out.println(Arrays.toString(arr));
}
其他:希尔排序,缩小增量排序
将多个有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。
public void mergeSortTest() {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
int[] temp = new int[arr.length]; //额外空间排序
mergeSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
private void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int middle = (left + right) / 2;
mergeSort(arr, left, middle, temp); //向左递归分解
mergeSort(arr, middle + 1, right, temp); //向右递归分解
merge(arr, left, middle, right, temp); //再合并
}
}
public void merge(int[] arr, int left, int middle, int right, int[] temp) {
int i = left; //左边初始序列索引
int j = middle + 1; //右边初始序列索引
int t = 0; //指向temp数组的当前索引
while (i <= middle && j <= right) {
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else {
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//剩余元素填充
while (i <= middle) {
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right) {
temp[t] = arr[j];
t += 1;
j += 1;
}
//将temp数组拷贝到arr
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
桶排序一种,将整数按照位数切割,然后按每个位数分别比较(消耗存储,容易OOM)
案例一 · 文件修改时间排序
class CompratorByLastModified implements Comparator<File> {
public int compare(File f1, File f2) {
long diff = f2.lastModified() - f1.lastModified();
if (diff > 0)
return 1;
else if (diff == 0)
return 0;
else
return -1;
}
public boolean equals(Object obj) {
return true;
}
}
案例二 · 文件名称排序
class CompratorByName implements Comparator<File> {
public int compare(File f1, File f2) {
if (f1.isDirectory() && f2.isFile())
return -1;
if (f1.isFile() && f2.isDirectory())
return 1;
return f2.getName().compareTo(f1.getName());
}
public boolean equals(Object obj) {
return true;
}
}
Arrays.sort(fs, new CompratorByLastModified());
分治算法的一种。二分搜索必须保证数据 有序
public static void main(String[] args) {
int[] arr = new int[]{1, 4, 5, 7, 8, 12, 45, 67, 99, 105};
System.out.println(biSearch(arr, 99));
}
public static int biSearch(int[] arr, int num) {
int start = 0;
int end = arr.length - 1;
int mid;
while (start <= end) {
System.out.println("*");
mid = (start + end) / 2;
if (arr[mid] == num) {
return mid + 1;
} else if (arr[mid] > num) { // 向左查找
end = mid - 1;
} else { // 向右查找
start = mid + 1;
}
}
return -1;
}
public static int biSearch2(int[] arr, int start, int end, int num) {
if (start > end) {
return -1;
}
int mid = (start + end) / 2;
int midVal = arr[mid];
if (midVal == num) {
return mid;
} else if (midVal > num) { // 向左查找
return biSearch2(arr, start, end - 1, num);
} else { // 向右查找
return biSearch2(arr, start + 1, end, num);
}
}
// 查找多个值的情况
类似于二分,不同的是每次从自适应mid处开始查找,适用于连续且量大的数据
公式: int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left])
注意需要判断findVal过大或过小越界的情况
数组、链表和二叉树的优缺点略
每个节点最有只有两个子节点(左节点、右节点)
满二叉树、完全二叉树
前序遍历:先输出父节点,再遍历左子树和右子树
中序遍历:先遍历左子树,再输出父节点,再遍历右子树
后序遍历:先遍历左子树,再遍历右子树,再输出父节点
左边比根节点小,右边比根节点大
二叉排序树既可以保证数据的检索速度,同时也可以保证增删改的速度
问题:极端情况,插入有序会退化成链表
public void insert(Tree tree, int value) {
if (tree.getValue() == 0) {
tree.setValue(value);
} else if (tree.getValue() < value) {
if (tree.getRight() == null) {
tree.setRight(new Tree());
}
insert(tree.getRight(), value);
} else if (tree.getValue() > value) {
if (tree.getLeft() == null) {
tree.setLeft(new Tree());
}
insert(tree.getLeft(), value);
}
}
平衡二叉树AVL的一种,降低树的高度,树高最大最小之差不超过1,实现方式:旋转(左、右、双旋转(左+右))。Java中的TreeSet底层用的就是红黑树。
左旋转:
Node newNode = new Node(value);
newNode.left = left;
newNode.right = right.left;
value = right.value;
right = right.right;
left = newNode;
多路搜索树,每个节点可以拥有多个孩子节点,进一步降低树的高度
一般用于文件系统的索引
在B树的基础上改造,它的数据都在叶子节点,同时叶子节点之间还加了指针形成链表
由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了
一般用于数据库索引(如果只取一条数据,Hash快)
赫(哈Huffman)夫曼树 带权路径长度最小的二叉树,也称最优二叉树
创建一个赫夫曼树
public static void main(String[] args) {
int[] arr = {10, 5, 8, 12, 9, 4, 14, 20};
Node huffman = create(arr);
// 遍历验证
huffman.print();
}
public static Node create(int[] arr) {
List<Node> nodes = new ArrayList<>();
for (int val : arr) {
nodes.add(new Node(val));
}
while (nodes.size() > 1) {
Collections.sort(nodes); //排序(从小到大)
Node leftNode = nodes.get(0); //取出最小
Node rightNode = nodes.get(1); //取出第二小
Node parent = new Node(leftNode.value + rightNode.value); //构建新的树
parent.left = leftNode;
parent.right = rightNode;
// 删除处理过的节点
nodes.remove(leftNode);
nodes.remove(rightNode);
nodes.add(parent);
}
return nodes.get(0);
}
static class Node implements Comparable<Node> {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
public void print() {
System.out.println(this.value);
if (this.left != null) {
this.left.print();
}
if (this.right != null) {
this.right.print();
}
}
@Override
public int compareTo(Node node) {
return this.value - node.value;
}
@Override
public String toString() {
return "Node{" + "value=" + value + '}';
}
}
应用:通讯领域信息传输、文件压缩解压,赫夫曼编码(按照各个字符出现的次数进行编码)
递归
当程序执行到一个方法时,就会独立开辟一个空间(栈),每个空间中的局部变量也是独立的。
注意递归调用处前后代码的执行顺序
public static void show(int n) {
System.out.println("n1: " + n); // 10 -> 2
if (n > 2) {
show(n - 1);
}
System.out.println("n2: " + n); // 2 -> 10
}
分治
待解决复杂的问题能够简化为几个若干个小规模相同的问题,然后逐步划分,达到易于解决的程度。应用:阶乘、斐波纳契数列、汉诺塔问题、棋盘覆盖、找出伪币、求最值
案例一 · 汉诺塔
public static void main() {
move(5, "A柱", "B柱", "C柱");
}
public static void move(int num, String a, String b, String c) {
if (num == 1) {
disPaly(num, a, c);
} else {
move(num - 1, a, c, b);
disPaly(num, a, c);
move(num - 1, b, a, c);
}
}
public static void disPaly(int num, String s1, String s2) {
System.out.println("第" + num + "个塔从" + s1 + "到" + s2);
}
案例二 · 走迷宫
public static void main(String[] args) {
int[][] map = {
{1, 1, 1, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 0, 1, 1, 1, 1},
{1, 1, 0, 0, 0, 0, 0, 1, 1},
{1, 0, 0, 1, 0, 1, 1, 1, 1},
{1, 0, 1, 1, 0, 1, 1, 1, 1},
{1, 0, 0, 1, 1, 1, 1, 1, 1},
{1, 0, 0, 0, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 1, 1, 1, 1, 1},
{1, 1, 1, 0, 0, 0, 0, 0, 1},
{1, 1, 1, 1, 1, 1, 1, 1, 1}
};
System.out.println("原来的地图 11 * 9");
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 9; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
// 1,1是起点 9,7是终点
setWay(map, 1, 1);
System.out.println("走过的地图");
for (int i = 0; i < 11; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j] == 2) {
System.out.format("\33[32;1m" + map[i][j] + "\33[0m ");
} else {
System.out.print(map[i][j] + " ");
}
}
System.out.println();
}
}
// 0-还没走 1-不可以走 2-可以走 3-走过不行
public static boolean setWay(int[][] map, int x, int y) {
if (map[9][7] == 2) {
return true;
} else {
if (map[x][y] == 0) {
map[x][y] = 2;
if (setWay(map, x + 1, y)) { //下
return true;
} else if (setWay(map, x, y + 1)) { //右
return true;
} else if (setWay(map, x - 1, y)) { //上
return true;
} else if (setWay(map, x, y - 1)) { //左
return true;
} else {
map[x][y] = 3;
return false;
}
} else {
return false;
}
}
}
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解,使用填表的方式。
与分治法不同的是,分解得到的子问题是非独立的,即下一个子阶段的求解是建立在上一个子阶段的解的基础上。
案例一 · 有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。
public static int[] steps = new int[11];
public static void main() {
steps[10] = calStep(10);
for (int i = 0; i < steps.length; i++) {
System.out.print(steps[i] + " ");
}
System.out.println();
System.out.println(steps[10]);
}
private static int calStep(int n) {
if (n == 1 || n == 2) {
return n;
}
if (steps[n - 1] == 0) {
steps[n - 1] = calStep(n - 1);
}
if (steps[n - 2] == 0) {
steps[n - 2] = calStep(n - 2);
}
return steps[n - 1] + steps[n - 2];
}
案例二 · 给定一个矩阵,从左上角开始每次只能向右走或者向下走,最后达到右下角的位置,路径中所有数字累加起来就是路径和,返回所有路径的最小路径和
public void step() {
int[][] arr = {
{4, 1, 5, 3},
{3, 2, 7, 7},
{6, 5, 2, 8},
{8, 9, 4, 5}
};
int result = minSteps(arr);
System.out.println("result=" + result);
}
private static int minSteps(int[][] arr) {
int row = arr.length;
int col = arr[0].length;
int[][] steps = new int[row][col];
steps[0][0] = arr[0][0];
for (int i = 1; i < row; i++) { //列+,从上到下
steps[i][0] = steps[i - 1][0] + arr[i][0];
}
for (int j = 1; j < col; j++) { //行+,从左到右
steps[0][j] = steps[0][j - 1] + arr[0][j];
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
steps[i][j] = Math.min(steps[i - 1][j], steps[i][j - 1]) + arr[i][j];
}
}
/*for (int i = 0; i < steps.length; i++) {
for (int i1 = 0; i1 < steps.length; i1++) {
System.out.print(steps[i][i1] + " ");
}
System.out.print("\n");
}*/
return steps[row - 1][col - 1];
}
//优化解法
public static int minSteps2(int[][] arr) {
int col = arr[0].length;
int[] steps = new int[col];
steps[0] = arr[0][0];
for (int i = 1; i < col; i++) {
steps[i] = steps[i - 1] + arr[0][i];//第一行
}
for (int i = 1; i < arr.length; i++) {
steps[0] = arr[i][0] + steps[0];//每一行的最左边
for (int j = 1; j < col; j++) {
steps[j] = Math.min(steps[j - 1] + arr[i][j], steps[j] + arr[i][j]);
}
}
System.out.println(Arrays.toString(steps));
return steps[col - 1];
}
在每一步求解的步骤中,它要求“贪婪”的选择最佳操作,并希望通过一系列的最优选择,能够产生一个问题的(全局的)最优解(不一定是最优,但会接近)。
案例一 · 有若干面额纸币,问得到总金额n需要最少的面额张数
private static int remain = 512;
public static void main(String[] args) {
Map<Integer, Integer> map = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
map.put(1, 3);
map.put(2, 1);
map.put(10, 1);
map.put(5, 2);
map.put(50, 3);
map.put(20, 2);
map.put(100, 5);
System.out.println(map);
Map<Integer, Integer> result = tanxin(map);
result.forEach((k, v) -> {
if (v != 0) {
System.out.println("需要面额为" + k + "的共" + v + "张");
}
});
}
public static Map<Integer, Integer> tanxin(Map<Integer, Integer> map) {
Map<Integer, Integer> result = new TreeMap<>();
map.forEach((k, v) -> {
int c = Math.min(remain / k, v);
remain = remain - c * k;
result.put(k, c);
});
return result;
}
KMP是一个解决某子串在文本串是否出现过的算法,简称字符串查找算法。
获取一个字符串(子串)的部分匹配值:如ABCDA前缀(A、AB、ABC、ABCD),后缀(BCDA、CDA、DA、A),此时匹配值为[0, 0, 0, 0, 1]
public void kmp() {
String str1 = "ABC ABBDDA ADABCDACDCB";
String str2 = "ABCDA";
int[] match = kmpMatch(str2);
System.out.println(Arrays.toString(match));
int index = kmpSearch(str1, str2, match);
System.out.println("index=" + index);
}
public static int kmpSearch(String str1, String str2, int[] match) {
for (int i = 0, j = 0; i < str1.length(); i++) {
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
// KMP算法核心
j = match[j - 1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) {
return i - j + 1;
}
}
return -1;
}
// 获得子串的部分匹配表
public static int[] kmpMatch(String dest) {
int[] match = new int[dest.length()];
match[0] = 0;
for (int i = 1, j = 0; i < dest.length(); i++) {
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = match[j - 1];
}
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
match[i] = j;
}
return match;
}
扩展:图(DFS-深度优先、BFS-广度优先)
算法:普里姆(Prim)算法
克鲁斯卡尔(Kruskal)算法
迪杰斯特拉(Dijkstra)算法
弗洛伊德(Floyd)算法