基础知识--算法

注:本文是个人学习算法的知识整理,大部分参考其他链接内容(都已附原链接地址),仅供参考。

算法

参考链接1:程序员必知的十大基础实用算法及其讲解

1.查找

1.1二分查找、插值查找

二分查找(折半查找):时间复杂度:O(log2N),只适用于已经排序好的静态查找表。
在这里插入图片描述
插值查找:适用于已经排序好的表,尤其是数据分布比较均匀的,数据分布不均匀的不适用。
在这里插入图片描述

1.2深度优先搜索(DFS)

深度优先搜索算法(Depth-First-Search),是搜索算法的一种。它沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。DFS属于盲目搜索。
深度优先遍历图算法步骤:
1、访问顶点v;
2、依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
3、若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。
详细介绍:图的遍历之 深度优先搜索和广度优先搜索

1.3广度优先搜索(BFS)

广度优先搜索算法(Breadth First Search),又称为"宽度优先搜索"或"横向优先搜索",简称BFS。

简单的说,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。BFS同样属于盲目搜索。一般用队列数据结构来辅助实现BFS算法。

它的思想是:从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得先被访问顶点的邻接点先于后被访问顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

换句话说,广度优先搜索遍历图的过程是以v为起点,由近至远,依次访问和v有路径相通且路径长度为1,2…的顶点。

算法步骤:
1、首先将根节点放入队列中。
2、从队列中取出第一个节点,并检验它是否为目标。
   如果找到目标,则结束搜寻并回传结果。
  否则将它所有尚未检验过的直接子节点加入队列中。
3、若队列为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”。
4、重复步骤2。
在这里插入图片描述

1.4线性查找算法(BFPRT)

BFPRT算法解决的问题十分经典,即从某n个元素的序列中选出第k大(第k小)的元素,通过巧妙的分析,BFPRT可以保证在最坏情况下仍为线性时间复杂度。该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达到o(n)的时间复杂度,五位算法作者做了精妙的处理。

算法步骤:
1、将n个元素每5个一组,分成n/5(上界)组。
2、取出每一组的中位数,任意排序方法,比如插入排序。
3、递归的调用selection算法查找上一步中所有中位数的中位数,设为x,偶数个中位数的情况下设定为选取中间小的一个。
4、用x来分割数组,设小于等于x的个数为k,大于x的个数即为n-k。
5、若i==k,返回x;若i<k,在小于x的元素中递归查找第i小的元素;若i>k,在大于x的元素中递归查找第i-k小的元素。
终止条件:n=1时,返回的即是i小元素。

2.排序

在这里插入图片描述
在这里插入图片描述

2.1冒泡排序

public static int[] bubbleSort(int[] intputArray) {
	int len = inputArray.length;
	for (int i = 0; i < len - 1; i++) {
		for (int j = 0; j < len - 1 - i; j++) {
			if (inputArray[j] > inputArray[j + 1]) {
				int temp = inputArray[j];
				inputArray[j] = inputArray[j + 1];
				inputArray[j + 1] = temp;
			}
		}
	}
	return inputArray;
}

2.2选择排序

public static int[] selectionSort(int[] arr) {
	int len = arr.length;
	for (int i = 0; i < len - 1; i++) {
		int minIndex = i;
		for (int j = i + 1; j < len; j++) {
			if (arr[j] < arr[minIndex]) {
				minIndex = j;
			}
		}
		int temp = arr[i];
		arr[i] = arr[minIndex];
		arr[minIndex] = temp;
	}
	return arr;
}

2.3插入排序

public static int[] insertionSort(int[] arr) {
	int len = arr.length;
	for (int i = 1; i < len - 1; i++) {
		int prefix = i - 1;
		int currentValue = arr[i];
		while ( prefix >= 0 && arr[prefix] > currentValue) {
			arr[prefix + 1] = arr[prefix];
			prefix--;
		}
		arr[prefix + 1] = currentValue;			
	}
	return arr;
}

2.4希尔排序

1959年shell发明,第一个时间复杂度突破O(n2)的排序算法,是简单插入排序的改进版,它们之间的区别是希尔排序会通过设定元素间的间隔,优先比较距离较远的元素。间隔由小到大变化,因此希尔排序又叫缩小增量排序
希尔排序的算法描述:通过设定元素间隔将整个待排序的序列分割成若干子序列分别进行直接插入排序,元素间隔根据一定的变化规则逐渐缩小直到为1,此时变为整个序列的直接插入排序。但此时与对原始序列直接进行插入排序的区别是:此时的序列经过前几轮的子序列插入排序,已经变为局部无序但整体有序的序列,再进行直接插入排序将会更加容易。

public static int[] shellSort(int[] arr) { 
	int len = arr.length;
	for (int gap = (int) Math.floor(len / 2); gap > 0; gap = (int) Math.floor(gap / 2)) { // 元素间隔按照一定规则不断缩小,直到为1变为整个序列的直接插入排序
		for (int i = gap; i < len; i++) {
			int currentValue = arr[i];
			int j = i;
			while (j - gap > 0 && currentValue < arr[j - gap]) {
				arr[j] = arr[j - gap];
				j = j - gap;
			}
			arr[j] = currentValue;
		}
	} 
	return arr;
}

2.5归并排序

该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

public static int[] mergeSort(int[] arr) {
	int len = arr.length;
	int middle = (int) Math.floor(len / 2);
	int[] leftArr = arr[]
}

2.6快速排序

2.7堆排序

2.8计数排序

2.9桶排序

2.10基数排序

3. 回溯算法

3.1递归函数

定义:自己调用自己的函数。
每个递归函数都有两个条件:停止条件和递归条件。
函数在递归的过程中,使用打函数调用栈,函数调用入栈,函数返回出栈,调用栈可能很长,将占用大量内存。

3.2回溯法介绍

回溯法和函数递归是离不开的,总体上是一种暴力搜索的算法。
回溯算法解决的问题,基本都能抽象成一个多叉树的结构,算法目的是对这个多叉树的所有结点完成遍历。

3.3经典问题

组合问题
排列问题
子集问题
切割问题
棋盘问题

3.4回溯算法模板

基本上所有的回溯算法代码,可以抽象成如下的算法代码模板:

void backtracking(函数参数) {
	if (停止条件) {
		收集结果;
		return;
	}
	for () { // 对当前结点集合进行遍历
	处理结点;
	函数递归;
	回溯(撤销)结点的处理;
	}
	return}

4.贪心算法

概念: 所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略的问题必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关)。所以,对所采用的贪心策略一定要仔细分析其是否满足无后效性。

基本思路:

  • 建立数学模型来描述问题
  • 把求解的问题分成若干个子问题
  • 对每个子问题求解,得到子问题的局部最优解
  • 把子问题的局部最优解合成原来问题的一个解

贪心选择的性质:
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。

该算法存在的问题:

  • 不能保证求得的最后解是最佳的
  • 不能用来求最大值或最小值的问题
  • 只能求满足某些约束条件的可行解的范围

贪心策略适用的问题前提是:

  • 问题具有最优子结构(当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。);
  • 问题具备无后效性;

贪心算法适用于:
1、原问题复杂度过高;
2、求全局最优解的数学模型难以建立;
3、求全局最优解的计算量过大;
4、没有太大必要一定要求出全局最优解,“比较优”就可以。

实现该算法的过程:
从问题的某一初始解出发;
while 能朝给定总目标前进一步 do
求出可行解的一个解元素;
由所有解元素组合成问题的一个可行解。

贪心算法的经典问题:

5.分治算法

参考链接:【五大常用算法】一文搞懂分治算法
算法介绍:
分治,字面上的解释是**“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题** ,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。在计算机科学中,分治法就是运用分治思想的一种很重要的算法。分治法是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)等等。

将父问题分解为子问题同等方式求解,这和递归的概念很吻合,所以在分治算法通常以递归的方式实现(当然也有非递归的实现方式)。分治算法的描述从字面上也很容易理解,分、治其实还有个合并的过程:

  • 分(Divide):递归解决较小的问题(到终止层或者可以解决的时候停下)
  • 治(Conquer):递归求解,如果问题够小直接求解。
  • 合并(Combine):将子问题的解构建父类问题

一般分治算法在正文中分解为两个即以上的递归调用,并且子类问题一般是不想交的(互不影响)。当求解一个问题规模很大很难直接求解,但是规模较小的时候问题很容易求解并且这个问题并且问题满足分治算法的适用条件,那么就可以使用分治算法。

分治算法的适用条件:

  1. 问题规模通常比较大,不易直接解决,但问题缩小到一定程度就能较容易的解决;
  2. 问题具有最优子结构,且子问题之间求解方式相同(似),且互不重叠,互相独立

分治算法经典用例:
1.二分查找(二分搜索),时间复杂度O(logn)
2.快速排序,时间复杂度O(nlogn)
在这里插入图片描述
在这里插入图片描述
3.归并排序
4.最近点对
5.最大子序列和
6.大整数乘法
7.汉诺塔问题

6.动态规划(Dynamic Programming)

参考:六大算法之三:动态规划
参考:看一遍就理解:动态规划详解

5.1 DP是什么

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。

动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算

动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题

5.2 DP的解题思路

什么样的问题适合使用DP解决?

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划

比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。

动态规划的解题思路

动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:

  • 穷举分析
  • 确定边界
  • 找出规律,确定最优子结构
  • 写出状态转移方程

------------------------------------我是一条可爱的分割线---------------------------------------------------------------
前言:
已知问题规模为n的前提A,求解一个未知解B。(我们用An表示“问题规模为n的已知条件”)。
通过将问题不断划分成规模更小的子问题,把问题规模降到0,即已知A0,可以得到A0->B。

  • 数学归纳法(马尔科夫模型):
    在这里插入图片描述
  • 第二数学归纳法(高阶马尔科夫模型、动态规划法):
    在这里插入图片描述
    动态规划(Dynamic Programming, DP)是运筹学的一个重要分支,是解决多阶段决策过程的最优化的一种方法,而不是一个具体的算法。通过将多阶段决策问题划分为一系列相互联系的单阶段问题,然后逐个解决。

DP算法的适用条件:

  • 最优子结构;
  • 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关
  • 有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

DP解题的一般思路:
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。
在这里插入图片描述
动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解

动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

使用动态规划求解问题,最重要的就是确定动态规划三要素:

(1)问题的阶段 
(2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。

递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。

确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态, 表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

动态规划与分治法的区别:
**动态规划算法:**动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。
**分治法:**子问题互相独立,互不影响;

运用DP的经典问题:

1.斐波那契数列
public class Solution {
	public static int finonacci(int n) {
		if (n == 0) {
			return 0;
		} else if (n == 1) {
			return 1;
		} else {
			int[] results = new int[n+1];
			results[0] = 0;
			results[1] = 1;
			for (int i = 2; i <= n; i++) {
				results[i] = results[i-1] + results[i-2]; // 确定状态,决策和状态转移方程
			}
			return results[n];		
	}
} 

与之类似的跳台阶问题:每次只能跳一级或二级台阶,跳到n级台阶共有多少种方法。

2.数组最大不连续递增子序列

问题: arr[] = {3,1,4,1,5,9,2,6,5}的最长递增子序列长度为4。即为:1,4,5,9
分析: 设置一个数组temp,长度为原数组长度,数组第i个位置上的数字代表0…i上最长递增子序列,当增加一个数字时,最大递增子序列可能变成前面最大的递增子序列+1,也可能就是前面最大递增子序列,这需要让新增加进来的数字arr[i]跟前面所有数字比较大小,即当 arr[i] > arr[j],temp[i] = max{temp[j]}+1,其中,j 的取值范围为:0,1…i-1,当 arr[i] < arr[j],temp[i] = max{temp[j]},j 的取值范围为:0,1…i-1,所以在状态转换方程为temp[i]=max{temp[i-1], temp[i-1]+1}。

public class Solution {
	public static int getMaxSequenceLength(int[] a) {
		int n = a.length;
		if (n == 0) {
			return 0;
		} else if (n == 1) {
			return 1;
		} else {
			int[] temp = new int[n]; // temp[i]代表0...i上最长递增子序列
			for (int i = 0; i < n; i++) {
				temp[i] = 1;
			}
			for (int i = 0; i < n; i++) {
				for (int j = 0; j < i; j++) {
					if ((a[i] > a[j]) && (temp[j] + 1 > temp[i])) { // 决策条件
						temp[i] = temp[j] + 1; // 状态转移方程
					}
				}
			}
			// 从temp数组里取出最大的值
			int maxLength = temp[0];
			for (int i = 0; i < n; i++) {
				if (maxLength < temp[i]) {
					maxLength = temp[i];
				}
			}
			return maxLength;
		}
	}
}
3.数组最大连续子序列和

如arr[] = {6,-1,3,-4,-6,9,2,-2,5}的最大连续子序列和为14。即为:9,2,-2,5

import java.lang.Math; // 默认导入
public class Solution {
	public static int getMaxSubSequenceSum(int[] a) {
		int n = a.length;
		if (n == 0) {
			return 0;
		}
		int sumValue = a[0];
		int maxValue = a[0];
		for (int i = 0; i < n; i++) {
			sum = Math.max(sum + a[i], a[i]); // 状态转移方程
			if (sum > max) {
				max = sum;
			}
		}
		return max;
	}
}
4.数字塔从上到下所有路径中和最大的路径

5.两个字符串最大公共子串

6.背包问题

问题描述:在N件物品中取出若干件放在容量为V的背包里,每件物品的体积为W1,W2……Wn(Wi为整数),与之相对应的价值为P1,P2……Pn(Pi为整数),求背包能够容纳的最大价值。
像这种固定数值的组合问题,适合使用DP方法。
动态规划的解法就是:创建一个二维数组,N行V列(为保证程序中数组不越界,实际需要创建N+1行V+1列)。其中,每一行表示可被挑选的各种元素,本题中就是指W1,W2……Wn;每一列表示背包的容量从小到大一直到v。则数组中每个位置(i,j)的数字就是当组成元素只有W1,W2……Wi时,背包可放容量为j时的结果,本题中就是容纳的最大价值。
所以很容易分析出,当(i,j)时,如果Wi能被放的下,空间减小,但是会增加Pi的价值,如果Wi不能被放的下,空间不变,是(i-1,j)的价值,取其中最大值就好了,即状态转化方程为能放的下,dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-w[i]]+p[i]);放不下,dp[i][j] = dp[i-1][j];

public class Solution {
	public static int getMaxValue(int[] w, int[] p, int v) {
		int n = w.length;
		if (n <= 0 || v <= 0) {
			return 0;
		}
		int[] dp = new int[n+1][v+1];
		for (int i = 1; i < n + 1; i++) {
			for (int j = 1; j < v + 1; j++) {
				if (j > w[i]) {
					/*
					 * 当能放得下这个物品时,放下这个物品,价值增加,但是空间减小,最大价值是dp[i-1][j-w[i]] + p[i]
					 * 当不放这个物品时,空间大,物品还是到i-1,最大价值是dp[i-1][j]
					 * 比较这两个大小,取最大的,就是dp[i][j]
					 */
					dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - w[i]] + p[i]);
				} else {
					dp[i][j] = dp[i - 1][j];
				}
			}
		}
		return dp[n][v]; // 数组最后一个元素就是所求解			
	}
}

优化:滚动数组方法,只创建一个一维数组,滚动更新其值,极大降低算法的空间复杂度;

public class Solution {
	public static int getMaxValue(int[] w, int[] p, int v) {
		int n = w.length;
		if (n <= 0 || v <= 0) {
			return 0;
		}
		int[] dp = new int[v+1];
		for (int i = 0; i < n; i++) {
			for (int j = 1; j < v + 1; j++) {
				if (j > w[i]) {
					dp[j] = Math.max(dp[j], dp[j - w[i]] + p[i]);
				} else {
					dp[j] = dp[j];
				}
			}
		}
		return dp[v];
	}
}
7.找零钱问题

7.Dijkstra算法

戴克斯特拉算法(Dijkstra’salgorithm)是由荷兰计算机科学家艾兹赫尔·戴克斯特拉提出。迪科斯彻算法使用了广度优先搜索解决非负权有向图单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
在这里插入图片描述

参考链接1:Dijkstra算法图文详解
参考链接2:Dijkstra算法原理

8.朴素贝叶斯分类算法

https://www.cnblogs.com/honkly/p/5317309.html
https://blog.csdn.net/xushiyu1996818/article/details/84762032
https://zhuanlan.zhihu.com/p/145074421
https://blog.csdn.net/qq_21397815/article/details/90289978

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值