算法设计==》

算法

  1. 俩数之和

算法设计与分析

什么是算法?

算法(algorithm)可以看作是任何良定义的计算过程,该过程取某个值或值的集合作为输入并产生某 个值或值的集合作为输出。这样算法就是把输人转换成输出的计算步骤的一个序列。 我们也可以把算法看成是用于求解某类计算问题的工具,问题陈述说明了期望的输人和输出对应的 关系。算法则描述一个特定的计算过程来实现该输入/输出关系。

如何学习算法?

学习算法的三个步骤

  • 实现算法
  • 验证算法的正确性
  • 分析算法的运行时间
前置知识
1.数据结构的分类与选择

数据结构是计算机存储、组织处理数据的方式,常见数据结构如下:

  • 线性结构:数据,链表,栈,队列,哈希表
  • 树形结构:二叉树,AVL树,2-3-4树,红黑树,B树,堆。。
  • 图形结构:领接矩阵,邻接表

按照速度的快慢来分类:数字和链表是最慢的,树相对较快,哈希表是最快的;

但最快的不代表是最好的方案,要根据实际情况分析选择,一般的可以参考下图进行选择;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TEg8H2Jx-1628239470356)(C:\Users\孤风\AppData\Local\Temp\WeChat Files\fae79651f7be598f44e525980f9d225.png)]

算法的评估

算法(Algorithm)是指用来操作数据、解决程序问题的一系列方法;对于同一个问题,使用不同的 算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别,比如下面这个 案例:

求第n个斐波那契数

public class Test {
/**
*
* 斐波那契数列:这个数列从第3项开始,每一项都等于前两项之和。
* 下标 0 1 2 3 4 5 6 7
* 数列 0 1 1 2 3 5 8 13
*/
//递归的方式:
public static long fun1(long n){
if(n<=1) return n;
return fun1(n-1)+fun1(n-2);
}
//普通循环的方式
public static long fun2(int n){
if(n<=1) return n;
long first=0;
long second=1;
for (long i = 0; i <n-1 ; i++) {
long sum=first+second;
first=second;
second=sum;
}
return second;
}
public static void main(String[] args) {
System.out.println(fun1(60));
System.out.println(fun2(60));
}
}

可以看到,实现方式有很多种,那么我们应该如何去衡量不同算法之间的优劣呢?这里就涉及到对 算法的评估。

事后统计法

通过统计、监控,利用计算机计时器对不同算法的运行时间进行比较,从而确定算法效率的高低, 但有非常大的局限性:

  • 测试结构非常依赖测试环境
  • 测试结构受数据规模的影响很大
事前分析估算

算法的执行效率,粗略地讲,就是算法代码执行的时间;但是,如何在不运行代码的情况下,用“肉 眼”得到一段代码的执行时间呢? 依据统计方法对算法进行估算:

时间维度:是指执行当前算法所消耗的时间,我们通常用[时间复杂度]来描述

空间维度:是指执行当前算法需要占用多少内存空间,我们通常用[空间复杂度]来计算

时间复杂度分析

公式:T(n)= O(f(n))

这种用大写O()来体现算法时间复杂度的记法,我们称之为大O阶记法它表示的并不是代码真正的 执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以也叫作渐进时间复杂度 (asymptotic time complexity),简称时间复杂度。这种表示方法描述的是运行时间的渐进上界, 也就是最坏情况下运行方法需要的时间,一般随着n的增大,T(n)增长最慢的算法为最优算法。

公式说明如下:

  • T(n)表示代码执行的时间;
  • n表示数据规模的大小;
  • f(n)表示每行代码执行的次数总和,因为这是一个公司,所以用f(n)来表示;
  • 公式中的O,表示代码执行时间T(n)与f(n)表达式成正比;
  • 公式中的低阶表达式,常量,系数等对增长趋势的影响不是很大,所以都可以忽略。
常见的时间复杂度量级

要确定某个算法的阶次,需要确定语句运行的次数,因此分析算法的复杂度关键就是要分析预计的运行情况。

1.常数阶O(1)

无论代码执行了多少行,只要是没有循环,方法调用等复杂结构,那这个代码的时间复杂度就是O(1),如:

void fun1(){//5
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
}
void fun01(int age) {
if(){
}else if (){
}else{
}
}

上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多 长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度,不管这个常数是多少,都记作O(1),而 不能记作O(3),O(12)等其他任何数字; 对于分支结构无论判断条件是真还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所 以单纯的分支结构(不包含在循环结构中),其时间复杂度都是O(1)。

线性阶O(n)

计算循环的阶次时,要确定循环体内语句运行的次数,单独的一个循环,没有加其它特殊的语句, 那这个代码的时间复杂度就是O(n),如:

void fun2(){
	for(int i=1;i<=n;i++){
		System.out.println(i);
	}
}

如果是for循环并列关系那么会执行2n次,由于常数可以忽略,所以也是O(n),如:

void fun2(){
for(int i=1;i<=n;i++){
System.out.println(i);//n
}
for(int i=1;i<=n;i++){
System.out.println(i);//n 一共是2n
}
}
3.对数阶O(logn)

循环体里面的条件决定循环的次数,如果该条件是指数级增长的话,我们称这个代码的时间复杂度 是O(logn),如:

int i = 1;
while(i<n){
	i = i * 2;//2^3
}

从上面代码可以看到,在while循环里面,每次都将判断条件 i 乘以 2,相当于是x个2相乘,也就是 2的x次方,在进行x次运算之后,i>=n终止循环,也就是2^x>=n,这里面我们重点关注这行代码执行的 次数,也就是要求x的值,在数学运算中

​ 当
a x = n 时 , 则 有 x = l o g a n 所 以 2 x = n 则 x = l o g 2 n a^x = n 时,则有 x=log_an 所以 2^x = n 则 x=log_2n ax=nx=logan2x=nx=log2n
在估算时间复杂度的时候,一般忽略底数,因此这个代码的时间复杂度为:O(logn)。

4.平方阶O(n^2)

双重循环意味着外出循环走一次,内层循环走一圈,所以代码执行的次数为外层循环的次数*内层循环的次数,我们称这个代码的时间复杂度是O(n^2)

for(i=1; i<=n; i++){ //n
for(j=1; j<=n; j++){//n
j = i;
j++;
}
}

这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²),如果将其中一层循环 的n改成m,即:

for(i=1; i<=m; i++){
	for(j=1; j<=n; j++){
		j = i;
		j++;
	}
}

那它的时间复杂度就变成了 O(m*n),所以总结循环的时间复杂度等于循环体的复杂度乘以该循环 运行的次数。下面这个循环嵌套它的时间复杂度又是多少呢?

for(i=0; i<n; i++){ //n
	for(j=i; j<n; i++){//n,n-1,n-2,,,,,1
		Sytem.out.println("111");
	}
}

由于i=0时,内循环执行了n次,当i=1时,执行了n-1次…当i=n-1时,执行了1次,所以总共执行 了:
n + ( n − 1 ) + ( n − 2 ) + . . . . . . + 1 = n ( n + 2 ) / 2 = 1 / 2 ∗ n 2 + 1 / 2 ∗ n n+(n-1)+(n-2)+......+1=n(n+2)/2 = 1/2*n^2+1/2*n n+(n1)+(n2)+......+1=n(n+2)/2=1/2n2+1/2n
使用推导大O阶的方法:去掉低阶表达式,然后忽略掉系数,所以最终为 O(n²)

线性对数阶O(nlogn)

线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时 间复杂度就是 n * O(logN),也就是了O(nlogN)。如:

for(m=1; m<n; m++){
	i = 1;
	while(i<n){
		i = i * 2;
	}
}

立方阶O(n³)、K次方阶O(n^k)参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类 似。

时间复杂度的比较

当n越来越大的时候,各个时间复杂度的大小排序如下
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( 2 n ) O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(2^n) O1<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)

如何提升n位数相乘的效率?—分治策略

许多有用的算法在结构上是递归的,即为了解决一个给定的问题,算法一次或多次递归地调用其自 身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想,也就是将原问题分解为几个规模 较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解,最终得到原问题 的解。

分治策略是常用的算法设计范例,分三个步骤来实现:

  • 分解:将原问题分解为若干子问题,这些子问题是原问题的规模较小的实例;
  • 解决:递归地求解各子问题
  • 合并:合并这些子问题的解组合成原问题的解。
第一个分治算法-KARATSUBA
MULTIPLY( x, y ):
if (n = 1):
return x·y
write x as a·10n/2 + b
write y as c·10n/2 + d
ac = MULTIPLY(a,c)
ad = MULTIPLY(a,d)
bc = MULTIPLY(b,c)
bd = MULTIPLY(b,d)
return ac·10n + (ad + bc)·10n/2 + bd
插入排序的实现

插入排序,适用于少量元素的排序,它的工作方式就像许多人排序扑克牌一样。开始时,我们的左 手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插人左手中正确的位置。为 了找到牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较,拿在左手上的牌总是排序好 的

伪代码实现

InsertionSort(A):
	for i in range(1,len(A));
		cur_value=A[i]
		j = i - 1
		while j>=0 and A[j] > cur_value:
			A[j+1] = A[j]
			j -= 1
		A[j+1] = cur_value

思想

  • 外层循环i遍历数组中的元素,内存循环j实现排序功能,每次都把拿到的元素放到合适的位置上去;
  • 每次与当前拿到元素的前一个元素进行比较,如果当前元素小于前一个元素,则把前一个元素向后移动,留出空位;然后继续比较,直到前一个元素不大于当前元素退出内层循环;
  • 把当前元素的值插入到j+1的位置上去;

java代码实现快速插入:

public class QuickInsert {
    /*快速插入排序,升序排列*/
    static void quickInsert(int []nums){

        for (int i = 0; i < nums.length; i++) {
            int curVal = nums[i];
            int j = i - 1;
            while (j>=0 && nums[j]>curVal){
                nums[j+1]=nums[j];
                j--;
            }
            nums[j+1]=curVal;
        }
    }

    public static void main(String[] args) {
        int nums[]={3,2,6,8,1,5,4,7};
        quickInsert(nums);
        for (int i = 0; i < nums.length; i++) {
            System.out.println(nums[i]);
        }
    }
}

插入排序的可行性

算法实现之后,需要通过某种方式证明该算法的可行性,也就是验证该算法在非特定输入的情况下都是正确的。

关注点

​ 可以看到,插入排序是一个迭代算法,那么每一次迭代有什么性质呢?

​ 每一次迭代都将在已排序数组部分插入一个新元素,也就是说,第i次迭代开始前,前i项元素是排好序的,第i此迭代完成后,前i+1项元素是排好序的;也就是说一个循环开始之前,循环执行的过程中以及循环执行结束后,始终保持该性质,我们把这种情况称为循环不变式,通过循环不变式来帮助我们验证算法的正确性。关于循环不变式,我们必须证明三条性质:

  • 初始化:循环的第一次迭代之前,它为真;
  • 保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真;
  • 终止:在循环终止时,不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。
例子:

初始化

首先证明在第一次循环迭代之前(当 i = 1 时),循环不变式成立。 此时子数组 A[0…i-1]仅由单个元素 A[0]组成,而且该子数组是排好序的。这表明第一次循环迭代之 前循环不等式成立。

保持

处理第二条性质:证明每次迭代都保持循环不变式。 每当遍历到A[i]元素时,都会通过比较将A[i]元素放到合适的位置上保证A[0]~A[i]之间的顺序,那么 对于下一次迭代时,前 i 项已排好序,所以循环不变式得以保持。

终止

最后研究在终止循环时,是否仍保持循环不变式。 循环终止时 i=n,此时 A[0…n-1] 由原来在 A[0…n-1] 中的元素组成,且已排好序,因此算法正确。

后续也将采用这种循环不变式的方法来验证算法的正确性。

插入排序的效率

通常使用大O记号来体现算法的时间复杂度,也就是通过统计语句执行的次数来估算方法的运行时 间,这种表示方法描述的是运行时间的渐进上界,也就是最坏情况下运行方法需要的时间。

插入排序最坏情况下的运行时间:

InsertionSort(A):
for i in range(1, len(A)):
cur_value = A[i]
j = i - 1
while j >= 0 and A[j] > cur_value:
A[j+1] = A[j]
j -= 1
A[j+1] = cur_value

通过分析可知,插入排序的执行总次数为4n+2n2次,根据大O的特性忽略掉系数、常熟、低阶,最终为 O(n2)。 很显然O(n^2)不是一个特别好的运行时间,那么问题来了,有没有更优的办法呢?

归并排序

归并排序算法完全遵循分治模式,操作如下:

  • 分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列
  • 解决:使用归并排序递归地排序两个子序列;
  • 解决:使用归并排序递归地排序两个子序列;
MERGESORT(A):
n = len(A) //数组长度
if n <= 1: //数组长度=1的时候,开始返回合并
return A
L = MERGESORT(A[0:n/2])
R = MERGESORT(A[n/2:n])
return MERGE(L,R)
MERGE(L,R):
result = length n array
i = 0, j = 0
for k in [0,...,n-1]:
if L[i] < R[j]:
result[k] = L[i]
i += 1
else:
result[k] = R[j]
j += 1
return result

Java代码实现归并排序:


归并排序递归地排序两个子序列;

  • 解决:使用归并排序递归地排序两个子序列;
MERGESORT(A):
n = len(A) //数组长度
if n <= 1: //数组长度=1的时候,开始返回合并
return A
L = MERGESORT(A[0:n/2])
R = MERGESORT(A[n/2:n])
return MERGE(L,R)
MERGE(L,R):
result = length n array
i = 0, j = 0
for k in [0,...,n-1]:
if L[i] < R[j]:
result[k] = L[i]
i += 1
else:
result[k] = R[j]
j += 1
return result

Java代码实现归并排序:

import java.util.Arrays;

public class MergeSort {
    public static int [] mergeSort(int []A){
       int []L;
       int []R;
       if (A.length <= 1){
           return A;
       }
        L=mergeSort(Arrays.copyOfRange(A, 0, A.length/2));
        R=mergeSort(Arrays.copyOfRange(A, A.length/2,A.length ));

        return merge(L,R);
    }
    public static int [] merge(int []L,int []R){
        int n = L.length+R.length;
        int []result = new int[n];
        int i = 0,j = 0,k = 0;
        while (i<L.length&&j<R.length){
            if (L[i]<R[j]){
                result[k++]=L[i++];
            }else {
                result[k++]=R[j++];
            }
        }
        while (i<L.length){
            result[k++] = L[i++];
        }
        while (j<R.length){
            result[k++]=R[j++];
        }
        return result;
    }
    public static void main(String[] args) {
        int nums[]={3,2,6,8,1,5,4,7,9,10,11,12,13};
        System.out.println(Arrays.toString(new MergeSort().mergeSort(nums)));
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值