算法设计原理有如下几种:
1.穷举法
也称枚举法,或暴力破解法。理论上可以破解任何密码,问题是如何缩短试误时间。
- 北魏时期,数学家张丘建在他的《算经》提出了“百鸡问题”。
- 世界近代三大数学难题之一的“四色猜想”。
- 排序算法中的,冒泡排序和选择排序。
上面几个问题都是穷举法的应用实例。
下面逐个介绍:
“百鸡问题”:
题目很简单:公鸡5文钱一只,母鸡3文钱一只,小鸡3只一文钱,用100文钱买一百只鸡,其中公鸡,母鸡,小鸡都必须要有,问公鸡,母鸡,小鸡要买多少只刚好凑足100文钱?
该问题导致三元不定方程组,其重要之处在于开创“一问多答”的先例。
要是手写的话,费时费力,下面用java程序来解决:
//该问题是个多解问题,要将多个答案都打印出来
//穷举法
public class BaiJiWenTi {
@Test
public void printYourIdear() {
System.out.println("百鸡问题的解有:(如下所示)");
//公鸡最大数
for (int x = 0 ; x <= 19 ; x++){
//母鸡最大数
for (int y = 0 ; y <= 31 ; y++){
//小鸡数
int z = 100-x-y;
if ( ((x*5 + y*3 + z/3) == 100) && (z%3 == 0) ){
System.out.println("公鸡:"+x + " 母鸡:"+y + " 小鸡:"+z);
}
}
}
}
}
百鸡问题的解有:(如下所示)
公鸡:0 母鸡:25 小鸡:75
公鸡:4 母鸡:18 小鸡:78
公鸡:8 母鸡:11 小鸡:81
公鸡:12 母鸡:4 小鸡:84
我们要知道目前的时间复杂度是O(N2),实际应用中这个复杂度是不能让你接受的,最多最多能让人接受的是O(N)。
下面来看“百鸡问题”第二种方式:
我们先将右边式子变形化简: x+y+z = 100 ① 5x+3y+z/3 = 100 ②
令②x3-① 可得
7x+4y = 100
=>y = 25-(7/4)x ③
又因为0 < y < 100 的自然数,则可令
x = 4k ④
将④代入③可得
=> y = 25-7k ⑤
将④⑤代入①可知
=> z = 75+3k ⑥
要保证 0 < x,y,z < 100 的话,k的取值范围只能是1,2,3
代码如下:
public class BaiJiWenTi {
@Test
public void printYourIdear() {
System.out.println("百鸡问题的解有:(如下所示)");
int x,y,z;
for (int k = 0 ; k < 4 ; k++){
x = 4*k;
y = 25 - 7*k;
z = 75 + 3*k;
System.out.println("公鸡:"+x + " 母鸡:"+y + " 小鸡:"+z);
}
}
}
打印和上面一样。
冒泡排序:
原理:
①. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
②. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
③. 针对所有的元素重复以上的步骤,除了最后一个。
④. 持续每次对越来越少的元素重复上面的步骤①~③,直到没有任何一对数字需要比较。
@Test
public void main() {
int [] a = {1,100,234,44,3,2,4,5};
bubbleSort(a);
}
//冒泡排序
public static void bubbleSort(int[] arr){
for (int i = arr.length - 1; i > 0; i--) { //外层循环移动游标
for(int j = 0; j < i; j++){ //内层循环遍历游标及之后(或之前)的元素
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp; //每次把大的数给右边
System.out.println("Sorting: " + Arrays.toString(arr));
}
}
}
}
第一轮把最大数移到a[n-1]处,第二轮把最大数移到a[n-2]处... 直到最小的数在a[0]处,就排序完了。
打印如下:时间复杂度很高
Sorting: [1, 100, 44, 234, 3, 2, 4, 5]
Sorting: [1, 100, 44, 3, 234, 2, 4, 5]
Sorting: [1, 100, 44, 3, 2, 234, 4, 5]
Sorting: [1, 100, 44, 3, 2, 4, 234, 5]
Sorting: [1, 100, 44, 3, 2, 4, 5, 234]
Sorting: [1, 44, 100, 3, 2, 4, 5, 234]
Sorting: [1, 44, 3, 100, 2, 4, 5, 234]
Sorting: [1, 44, 3, 2, 100, 4, 5, 234]
Sorting: [1, 44, 3, 2, 4, 100, 5, 234]
Sorting: [1, 44, 3, 2, 4, 5, 100, 234]
Sorting: [1, 3, 44, 2, 4, 5, 100, 234]
Sorting: [1, 3, 2, 44, 4, 5, 100, 234]
Sorting: [1, 3, 2, 4, 44, 5, 100, 234]
Sorting: [1, 3, 2, 4, 5, 44, 100, 234]
Sorting: [1, 2, 3, 4, 5, 44, 100, 234]
选择排序:
- 从待排序序列中,找到值最小的元素
- 如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换
- 从余下的 N - 1 个元素中,找出关键字最小的元素,重复①、②步,直到排序结束。
@Test
public void main() {
int [] a = {1,100,234,44,3,2,4,5};
selectionSort(a);
}
//选择排序
public static void selectionSort(int[] arr){
for(int i = 0; i < arr.length-1; i++){
int min = i;
for(int j = i+1; j < arr.length; j++){ //选出之后待排序中值最小的位置
if(arr[j] < arr[min]){
min = j;
}
}
if(min != i){
int temp = arr[min]; //交换操作
arr[min] = arr[i];
arr[i] = temp;
System.out.println("Sorting: " + Arrays.toString(arr));
}
}
}
每次都把最小的数给第一个位置,从0到n-1。时间复杂度还是很高。
打印如下:
Sorting: [1, 2, 234, 44, 3, 100, 4, 5]
Sorting: [1, 2, 3, 44, 234, 100, 4, 5]
Sorting: [1, 2, 3, 4, 234, 100, 44, 5]
Sorting: [1, 2, 3, 4, 5, 100, 44, 234]
Sorting: [1, 2, 3, 4, 5, 44, 100, 234]
2.回溯法
尽管回溯法也算是暴力方法,但也不是特别暴力。
是一种优先搜索法,按选优条件向前搜索,当某一步走不通时,就退一步重新选择再走。
而满足回溯条件的某个状态的点称为“回溯点”。
“八皇后问题”,是回溯算法的经典例子。
“八皇后问题”:
介绍:
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。(92种)
算法思考,初步思路:
构建二维int或者short型数组,内存中模拟棋盘
chess[r][c]=0表示:r行c列没有皇后,chess[r][c]=1表示:r行c列位置有一个皇后
从第一行第一列开始逐行摆放皇后
依题意每行只能有一个皇后,遂逐行摆放,每行一个皇后即可
摆放后立即调用一个验证函数(传递整个棋盘的数据),验证合理性,安全则摆放下一个,不安全则尝试摆放这一行的下一个位置,直至摆到棋盘边界
当这一行所有位置都无法保证皇后安全时,需要回退到上一行,清除上一行的摆放记录,并且在上一行尝试摆放下一位置的皇后(回溯算法的核心)
当摆放到最后一行,并且调用验证函数确定安全后,累积数自增1,表示有一个解成功算出
验证函数中,需要扫描当前摆放皇后的左上,中上,右上方向是否有其他皇后,有的话存在危险,没有则表示安全,并不需要考虑当前位置棋盘下方的安全性,因为下面的皇后还没有摆放
/**
* 在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,
* 即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
* 下面使用递归方法解决
* @author newflydd@189.cn
*
*/
public class EightQueen {
private static final short N=8; //使用常量来定义,方便之后解N皇后问题
private static int count=0; //结果计数器
@Test
public void main() {
Date begin =new Date();
//初始化棋盘,全部置0
short chess[][]=new short[N][N];
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
chess[i][j]=0;
}
}
putQueenAtRow(chess,0);
Date end =new Date();
System.out.println("解决 " +N+ " 皇后问题,用时:" +String.valueOf(end.getTime()-begin.getTime())+ "毫秒,计算结果:"+count);
}
private static void putQueenAtRow(short[][] chess, int row) {
/**
* 递归终止判断:如果row==N,则说明已经成功摆放了8个皇后
* 输出结果,终止递归
*/
if(row==N){
count++;
System.out.println("第 "+ count +" 种解:");
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
System.out.print(chess[i][j]+" ");
}
System.out.println();
}
return;
}
short[][] chessTemp=chess.clone();
/**
* 向这一行的每一个位置尝试排放皇后
* 然后检测状态,如果安全则继续执行递归函数摆放下一行皇后
*/
for(int i=0;i<N;i++){
//摆放这一行的皇后,之前要清掉所有这一行摆放的记录,防止污染棋盘
for(int j=0;j<N;j++)
chessTemp[row][j]=0;
chessTemp[row][i]=1;
if( isSafety( chessTemp,row,i ) ){
putQueenAtRow(chessTemp,row+1);
}
}
}
private static boolean isSafety(short[][] chess,int row,int col) {
//判断中上、左上、右上是否安全
int step=1;
while(row-step>=0){
if(chess[row-step][col]==1) //中上
return false;
if(col-step>=0 && chess[row-step][col-step]==1) //左上
return false;
if(col+step<N && chess[row-step][col+step]==1) //右上
return false;
step++;
}
return true;
}
}
共92种打印。
3.递归法
把问题转化为规模缩小了的同类问题的子问题,是一种直接或间接调用自身算法的过程。
“斐波拉契数列”又称黄金分割数列,是递归法的经典例子。
“汉诺塔问题”,也是递归算法的经典例子。
"斐波拉契数列":另一篇博客
https://blog.csdn.net/qq_38261174/article/details/80631596
“汉诺塔问题”:
介绍:
汉诺塔是一个发源于印度的益智游戏,也叫河内塔。相传它源于印度神话中的大梵天创造的三个金刚柱,一根柱子上叠着上下从小到大64个黄金圆盘。大梵天命令婆罗门将这些圆盘按从小到大的顺序移动到另一根柱子上,其中大圆盘不能放在小圆盘上面。当这64个圆盘移动完的时候,世界就将毁灭。
那么好多人会问64个圆盘移动到底会花多少时间?那么古代印度距离现在已经很远,这64个圆盘还没移动完么?我们来通过计算来看看要完成这个任务到底要多少时间?
我们首先利用数学上的数列知识来看看F(n=1)=1,F(n=2)=3,F(n=3)=7,F(n=4)=15……F(n)=2F(n-1)+1;
我们使用数学归纳法可以得出通项式:F(n)=2^n-1。当n为64时F(n=64)=18446744073709551615。
我们假设移动一次圆盘为一秒,那么一年为31536000秒。那么18446744073709551615/31536000约等于584942417355天,换算成年为5845.54亿年。
目前太阳寿命约为50亿年,太阳的完整寿命大约100亿年。所以我们整个人类文明都等不到移动完整圆盘的那一天。
算法:当只有一个盘子的时候,只需要从将A塔上的一个盘子移到C塔上。
当A塔上有两个盘子是,先将A塔上的1号盘子(编号从上到下)移动到B塔上,再将A塔上的2号盘子移动的C塔上,最后将B塔上的小盘子移动到C塔上。
当A塔上有3个盘子时,先将A塔上编号1至2的盘子(共2个)移动到B塔上(需借助C塔),然后将A塔上的3号最大的盘子移动到C塔,最后将B塔上的两个盘子借助A塔移动到C塔上。
当A塔上有n个盘子是,先将A塔上编号1至n-1的盘子(共n-1个)移动到B塔上(借助C塔),然后将A塔上最大的n号盘子移动到C塔上,最后将B塔上的n-1个盘子借助A塔移动到C塔上。
综上所述,除了只有一个盘子时不需要借助其他塔外,其余情况均一样(只是事件的复杂程度不一样)
代码如下:
int i=1;//记录步数
void move(int n,char from,char to) //将编号为n的盘子由from移动到to
{System.out.printf("第%d步:将%d号盘子%c---->%c\n",i++,n,from,to);
}
void hanoi(int n,char from,char denpend_on,char to)//将n个盘子由初始塔移动到最后塔(利用中间塔)
{
if (n==1)
move(n,from,to);//只有一个盘子是直接将初塔上的盘子移动到 最后塔
else
{
hanoi(n-1,from,to,denpend_on);//先将初始塔的前n-1个盘子借助 最后的塔 移动到 中间的塔
move(n,from,to); //将剩下的一个盘子移动到 最后的塔
hanoi(n-1,denpend_on,from,to);//最后将 中间塔 上的n-1个盘子移动到 最后的塔
}
}
@Test
public void main()
{
int n=3;
System.out.printf("盘子3个");//从上到下为1号,2号,3号盘子
char x='A',y='B',z='C'; //3根柱子 A B C
System.out.printf("盘子移动情况如下:\n");
hanoi(n,x,y,z);
}
盘子3个盘子移动情况如下:
第1步:将1号盘子A---->C
第2步:将2号盘子A---->B
第3步:将1号盘子C---->B
第4步:将3号盘子A---->C
第5步:将1号盘子B---->A
第6步:将2号盘子B---->C
第7步:将1号盘子A---->C
4.分治法
“分而治之”。把一个复杂问题分成两个或多个相同或近似的子问题,子问题再分...直到直接求解,再将子解合并,就是原解。
例如排序算法中的“快速排序”和“归并排序”
“快速排序”:
- 定义两个索引 i 和 j ,分别指向数组的第一个元素和最后一个元素,先找一个中轴元素,一般是第一个元素,保存在key中;
- j 从右往左找(j –-),找第一个小于key的数,如果找到就把 j 指向的数放在 i 指向的位置,i ++;
- i 从左往右找(i ++),找第一个大于等于key的数,如果找到就把 i 指向的数放在 j 指向的位置,j–-;
- 循环上面两步,直到 i 和 j 相遇(i >= j),把 key 放在 i 指向的位置。至此,一趟排序结束,key里保存的数字放到了它最终的位置上,并且它左面的数字都比它小,它右面的数字都比它大。
public static void sort(int[] a, int low, int high) {
if(low>=high)
return;
int i = low;
int j = high;
int key = a[i];
while (i < j) {
while (i < j && a[j] >= key)
j--;
a[i++] = a[j];
while (i < j && a[i] <= key)
i++;
a[j--] = a[i];
}
a[i] = key;
sort(a,low,i-1);
sort(a,i+1,high);
}
public static void quickSort(int[] a) {
sort(a, 0, a.length-1);
for(int i:a)
System.out.print(i+" ");
}
@Test
public void main() {
int[] a = { 49, 38, 65, 97, 76, 13, 27, 50 };
quickSort(a);
}
“归并排序”:
static int number=0;
@Test
public void main() {
int[] a = {24, 5, 98, 28, 99, 56, 34, 2 };
printArray("排序前:",a);
MergeSort(a);
printArray("排序后:",a);
}
private static void printArray(String pre,int[] a) {
System.out.print(pre+"\n");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+"\t");
System.out.println();
}
private static void MergeSort(int[] a) {
// TODO Auto-generated method stub
System.out.println("开始排序");
Sort(a, 0, a.length - 1);
}
private static void Sort(int[] a, int left, int right) {
if(left>=right)
return;
int mid = (left + right) / 2;
//二路归并排序里面有两个Sort,多路归并排序里面写多个Sort就可以了
Sort(a, left, mid);
Sort(a, mid + 1, right);
merge(a, left, mid, right);
}
private static void merge(int[] a, int left, int mid, int right) {
int[] tmp = new int[a.length];
int r1 = mid + 1;
int tIndex = left;
int cIndex=left;
// 逐个归并
while(left <=mid && r1 <= right) {
if (a[left] <= a[r1])
tmp[tIndex++] = a[left++];
else
tmp[tIndex++] = a[r1++];
}
// 将左边剩余的归并
while (left <=mid) {
tmp[tIndex++] = a[left++];
}
// 将右边剩余的归并
while ( r1 <= right ) {
tmp[tIndex++] = a[r1++];
}
System.out.println("第"+(++number)+"趟排序:\t");
// TODO Auto-generated method stub
//从临时数组拷贝到原数组
while(cIndex<=right){
a[cIndex]=tmp[cIndex];
//输出中间归并排序结果
System.out.print(a[cIndex]+"\t");
cIndex++;
}
System.out.println();
}
5.贪心法
又称“贪心算法”。经典问题是“旅行商问题”。
6.动态规划
例子:GPS寻找最优路径的算法就是动态规划算法。
再实际生活中应用广泛。