算法与数据结构 - 算法基础

文章目录

  • 前言
  • 一、 算法与数据结构简介
    • 1. 算法
      • 1.1 什么是算法
      • 1.2 算法的作用
      • 1.3 题外话
    • 2. 数据结构
      • 2.1 什么是数据结构
  • 二、评价算法的标准
    • 2.1 时间复杂度
      • (1)基础运行次数(时间频次)
      • (2)渐进时间复杂度
        • 什么是渐进时间复杂度
        • 如何推导时间复杂度
        • 常见的时间复杂度
        • 常见复杂度排序
    • 2.2 空间复杂度
      • (1)常见的空间复杂度
  • 结语

前言

点赞再看,养成习惯!

关注晓龙oba公众号,更多电子书及学习资源免费领取。

一、 算法与数据结构简介

1. 算法

1.1 什么是算法

我们可以简单的概括,算法其实就是一系列程序问题的 解决方案。算法通常由一系列用来解决问题的指令构成,一个良好的算法通常只需要按照一定的规范输入,就可以在有限的时间内获得所需要的题解。

# 我们日常生活中会遇到很多需要算法的场景
# 比如 我们需要将 153245个数字按照从小到大的顺序进行排序
# 而我们排序的方案就是算法(这里先不介绍具体的排序方法)

1.2 算法的作用

提到算法的作用,我相信很多同学下意识想到的就是面试 。没错,现在国内互联网越来越卷的大格局下,很多大厂都将算法作为面试的敲门砖,但这个作用也仅仅局限在面试阶段。实际上算法作为数学的一部分,它为我们带来的可不仅仅是一份工作那么简单。

1. 比如我们日常翻阅字典的行为,首先通过首字母确认单词所在的页码再去对应的页码查找的行为在广义上就是一种检索算法。
2. 电视剧中的电报其实就是一种对称加密算法。
3. 当我们打开手机进行导航时,APP为我们提供的路径就是通过算法计算出的最短路径。

1.3 题外话

很多时候我们总是陷入一种逻辑错误,我们听别人说算法只会用到面试中,他不重要,算法无用这种言论。这种辩解大多数的前提都是我不会,所以他不重要,我没错的思维出发的。我们要知道有剑不拔和无剑可拔的区别,不要让我们在需要的时候才想起自己并不具备这方面的能力而错失难得的机会。

2. 数据结构

2.1 什么是数据结构

数据结构本质是一种逻辑规则。它是用来表示数据与数据之间的元素特性。在计算机领域其主要就是用来表示计算机存储数据的方式及数据与数据之间的关联关系。

二、评价算法的标准

2.1 时间复杂度

每当我们讨论一个算法的时候,避不开的会讨论一个算法的运行时间,相比之下,我们会更加倾向于效率更高的算法,以最大限度地减少运行时间或者说是程序占用资源的时间。那么我们应该如何去衡量一个算法的效率呢?通过程序的运行时间码?很遗憾,由于程序每次运行时获得的资源不同,变量不同等差异,即使是相同的算法每一次运行的时间也是存在差异的,很明显我们无法简单的通过运行时间来判断一个算法的优略,于是我们只能够去寻找其他方法。

(1)基础运行次数(时间频次)

我们先来通过一个简单的场景来感受下影响代码运行的原因。

我们需要写一个简单的程序将a,b,c,d四个小写字母转化为大写

让我们来写个非常简单的代码实现:

public class UpperCase {

    public static void main(String[] args) {
        /*创建一个英文字母数组*/
        String[] letters = {"a", "b", "c", "d"};
        for (int i = 0; i < letters.length; i++) {
            System.out.print("字母"+letters[i]+"开始转换,");
            letters[i] = letters[i].toUpperCase();
            System.out.println("转换结果:"+letters[i]);
        }
    }
}

运行结果:
算法与数据结构 - 算法基础_第1张图片
分析:
上面的系统中我们经历了几次循环呢?答案很简单,就是4次。如果我们将字母的个数增加到26个呢?理所当然的,程序会进行26次运算。我们可以用一个简单的函数来表示这个相对的时间就是:
T(n) = n

(2)渐进时间复杂度

当我们获得了一个线性的函数T(n) = n 是否就可以比较一个算法优劣了呢?当然不行。这是由于我们的函数中还是存在一个不确定的变量n,假设此时我们有两个算法。
算法A: T(n) = 10n
算法B: T(n) = 2n*3
是否算法B的运行时间就一定大于算法A呢?这就要取决于n的取值了。比如当n取值为1时:
算法A: T(n) = 10
算法B: T(n) = 6
此时明显的算法B要优于算法A,此时我们将n替换为2:
算法A: T(n) = 10
算法B: T(n) = 12
仅仅是更换一个变量,算法B的运行时常就高于算法A了。所以我们无法简单的将程序的基础运算次数作为衡量代码优劣的标准。这个时候我们就引入了一个渐进时间复杂度(asymtotic time complexity) 的概念。

什么是渐进时间复杂度

我们先来看一下枯燥的理论概念:

若存在函数f(n),使得n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则可以称f(n)为T(n)的同数量级函数,记作T(n) = O(f(n)),则O(f(n))被称为当前算法的渐进时间复杂度,简称为时间复杂度,用大写字母O来表示,因此也被称为大O表示法。

这段理论概念主要是表达了程序的基础运行次数T(n)和
时间复杂度O(n)之间的关系,简单地说时间复杂度就是将基础运行次数简化为一个数量级,这个数量级是抛开了一些系数影响的。那我们该如何求得一个O(n)呢?

如何推导时间复杂度

我们推导时间复杂度需要基于时间频次来,其主要原则有三点:
1. 若运行时间是常数量级,则用常数1表示
2. 只保留时间函数中的最高阶项,如T(n) = n^3 + 3n 则O(T(n)) = n^3
3. 若最高阶存在,则省去最高阶前的系数,如T(n) = 3n^3 + 3n 则O(T(n)) = n^3

对于上面的三个原则,我们也可以总结出一些经验来帮助我们分析时间复杂度:
1. 我们只需要关注代码中循环次数最多的代码段,对应的就是原则2中提到的我们只需要保留最高阶项
2. 代码的整体复杂度等于代码中存在的嵌套代码与外层代码之间负载度的乘积

常见的时间复杂度

1. 常数阶 O(1)
这应该是最常见的时间复杂度,这种复杂度表示变量发生了什么变化,执行时间都是恒定的。这种复杂度也被我们称为常数阶,我们来写个代码做简单的举例:

    public static void main(String[] args) {
        /*1. O(1) 常数阶*/
        Integer n = 5 ;
        System.out.println("n等于:"+n);
    }

在这段程序中,无论我们如何改变n的值,都不会影响程序的执行频次。

2. 线性阶 O(n)
线性阶通常出现在存在一层循环的程序中,随着n的增加,循环次数也会跟随着增加。比如下面的这段代码:

        /*2. O(n) 线性阶*/
        Integer n = 5 ;
        for (int i = 0; i < n; i++) {
            System.out.println("程序运行第"+(i+1)+"次");
        }

这里随着n的变大,程序的循环次数也会增加。
3. 对数阶 O(logn)
当一个算法可以成倍的缩减运行次数的时候,此时的代码负载度常为对数阶。比如很常见的二分查找:

class Solution {
    public int search(int[] nums, int target) {
		int index = -1;
		// 先创建集合
		List<Integer> collect = Arrays.stream(nums).boxed().collect(Collectors.toList());
		// 二分查找法 传入数组和目标值
		Boolean flag = true;
		Integer mark = collect.size() < 3 ? 0 : collect.size() / 2;
		Integer max = collect.size();
		Integer min = 0;
		while (flag) {
			// 判断左右
			if (collect.get(mark) == target) {
				index = mark;
				flag = false;
			} else if (collect.get(mark) < target) {
				// 拆分到数组右侧 获取中位数
				min = mark;
				mark = (max + mark) / 2;
			} else if (collect.get(mark) > target) {
				// 左侧
				max = mark;
				mark = (min + mark) / 2;
			}
			if (mark >= max || mark <= min) {
                index =	collect.get(mark) == target ?mark :index ;
				flag = false;
			}

		}
		return index;
    }
}

假设我们一共有n个元素,那我们第一次运算后还剩下:
num= n/2 个元素
第二次运算后就是
num= n/ 2/2 个元素
那么第x次查找后则剩下
num = n / (2^x)个元素
当我们的x设为最大值,则最后num只剩下一个元素时:
1 = n/(2^x)

2^x = n ;
因此当我们想要分析最多需要多少次时就可以推得:
x = log2n
从而推出
O(n) = log2n

4. 平方阶 O(n^2)
常见于两层循环:

        Integer n = 5 ;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n ; j++) {
                System.out.println("输出");
            }
        }

常见复杂度排序

O(1)

2.2 空间复杂度

空间复杂度理解起来就要简单很多,通常就是只一个程序运行时需要占用的内存空间大小。利用程序的空间复杂度可以让我们对程序运行时所需要的内存空间有一个初步的预估。每一个程序在运行时其存储空间可以分为两部分构成:

  1. 固定部分: 这部分空间是不受程序外部变量的影响的,其主要包含指令空间(代码空间),数据空间(常量,变量空间)等所占的内存,属于静态空间。
  2. 可变部分: 这部分主要是程序动态分配的内存空间,通常与变量和算法有关,一个算法所需要的存储空间常用f(n)表示。

大多数情况下,一个程序的时间复杂度和空间复杂度往往是互相影响的,其之间的关系通常都是反比。简单来说就是当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。

(1)常见的空间复杂度

空间复杂度比较常用的有:O(1)、O(n)、O(n²)三种。
其中O(1)表示当前算法所占用的空间并不会随着问题复杂度的增加而改变。O(n)常见于循环之中而O(n²)则常见于递归或多层循环。

结语

今天的内容就到此结束了,有疑问的小伙伴欢迎评论区留言或者私信博主,博主会在第一时间为你解答。
Spring通用架构及工具已上传到gitee仓库,需要的小伙伴们可以自取:
https://gitee.com/xiaolong-oba/common-base
屏幕前努力学习的你如果想要持续了解博主最新的学习笔记或收集到的资源,可以关注博主的个人公众号。这里有很多最新的技术领域PDF电子书及好用的软件分享算法与数据结构 - 算法基础_第2张图片

码字不易,感到有收获的小伙伴记得要关注博主一键三连,不要当白嫖怪哦~
如果大家有什么意见和建议请评论区留言或私聊博主,博主会第一时间反馈的哦。

你可能感兴趣的:(算法与数据结构,算法,数据结构,时间复杂度,空间复杂度)