肝数据结构与算法第一天笔记与(java版)

内容来源力扣和算法书籍。

一、数据结构

众所周知程序 = 数据结构 + 算法,因此数据结构有那些呢?

1、线性结构

线性结构是最简单的数据结构,包括数组、链表,以及由它们衍生 出来的栈、队列、哈希表。

2、树

树是相对复杂的数据结构,其中比较有代表性的是二叉树,由它又 衍生出了二叉堆之类的数据结构。

3、 图

图是更为复杂的数据结构,因为在图中会呈现出多对多的关联关系。

4、其他数据结构

除上述所列的几种基本数据结构以外,还有一些其他的千奇百怪的 数据结构。它们由基本数据结构变形而来,用于解决某些特定问题,如 跳表、哈希链表、位图等。

二、算法的复杂度

算法最重要的指标就是这两个家伙:时间复杂度与空间复杂度。说实话我对我这个数学不好的人来说说这两个家伙真的是太难顶了。

难顶哇(杰尼龟流泪表情包)_杰尼龟_难顶_流泪表情

 

1、时间复杂度

什么是时间复杂度举个栗子:

我和老王写了一段代码功能一样、内存一样、我的运行时间是50s,老王的是5s。好家伙直接给吊打这怎么玩只能把时间复杂度搞懂了。

根据定义,时间复杂度指输入数据大小为 N 时,算法运行所需花费的时间。

要注意的是统计的是算法的「计算操作数量」,而不是「运行的绝对时间」。计算操作数量和运行绝对时间呈正相关关系,并不相等。(算法的运行速度受多方面因素影响,比如你的电脑配置,计算机语言,使用本地 IDE 或力扣平台提交)

 直白地讲,时间复杂度就是把程序的相对执行时 间函数T(n)简化为一个数量级,这个数量级可以是n、n 2 、n 3 等。

2、常见时间复杂度种类

根据从小到大排列,常见的算法时间复杂度主要有:

O(1)<O(logN)<O(N)<O(NlogN)<O(N2)<O(2N)<O(N!)


来源:力扣(LeetCode) 

1.常数 O(1) :

运行次数与 N大小呈常数关系,即不随输入数据大小 N 的变化而变化。

int algorithm(int N) {
    int a = 1;
    int b = 2;
    int x = a * b + N;
    return 1;
}

无论 a 取多大,都与输入数据大小 N 无关,因此时间复杂度仍为 O(1)。

int algorithm(int N) {
    int count = 0;
    int a = 10000;
    for (int i = 0; i < a; i++) {
        count++;
    }
    return count;
}

2.线性 O(N) :

循环运行次数与 N 大小呈线性关系,时间复杂度为 O(N)。

int algorithm(int N) {
    int count = 0;
    for (int i = 0; i < N; i++)
        count++;
    return count;
}

对于以下代码,虽然是两层循环,但第二层与 N 大小无关,因此整体仍与 N 呈线性关系

int algorithm(int N) {
    int count = 0;
    int a = 10000;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < a; j++) {
            count++;
        }
    }
    return count;
}

3.平方 O(N^2) 

两层循环相互独立,都与 N 呈线性关系,因此总体与 N 呈平方关系,时间复杂度为 O(N^2) 。 

int algorithm(int N) {
    int count = 0;
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            count++;
        }
    }
    return count;
}

4. 指数 O(2^N)

生物学科中的 “细胞分裂” 即是指数级增长。初始状态为 1 个细胞,分裂一轮后为 2 个,分裂两轮后为 4 个,……,分裂 N轮后有 2^N2个细胞。 

算法中,指数阶常出现于递归,算法原理图与代码如下所示。

int algorithm(int N) {
    if (N <= 0) return 1;
    int count_1 = algorithm(N - 1);
    int count_2 = algorithm(N - 1);
    return count_1 + count_2;

}

5.阶乘 O(N!): 

阶乘阶对应数学上常见的 “全排列” 。即给定 N 个互不重复的元素,求其所有可能的排列方案,则方案数量为:N×(N−1)×(N−2)×⋯×2×1=N!

如下图与代码所示,阶乘常使用递归实现,算法原理:第一层分裂出 N 个,第二层分裂出 N - 1 个,…… ,直至到第 NN层时终止并回溯。

int algorithm(int N) {
    if (N <= 0) return 1;
    int count = 0;
    for (int i = 0; i < N; i++) {
        count += algorithm(N - 1);
    }
    return count;
}

 6.对数 O(\log N) :

对数阶与指数阶相反,指数阶为 “每轮分裂出两倍的情况” ,而对数阶是 “每轮排除一半的情况” 。对数阶常出现于「二分法」、「分治」等算法中,体现着 “一分为二” 或 “一分为多” 的算法思想。

设循环次数为 mm ,则输入数据大小 N与 2 ^ m2 ,  呈线性关系,两边同时取 log_2 。  对数,则得到循环次数 m 与 log_2 N 。

int algorithm(int N) {
    int count = 0;
    float i = N;
    while (i > 1) {
        i = i / 2;
        count++;
    }
    return count;
}

7.线性对数 O(N \log N): 

两层循环相互独立,第一层和第二层时间复杂度分别为 O(\log N) 和 O(N) ,则总体时间复杂度为 O(N \log N)

int algorithm(int N) {
    int count = 0;
    float i = N;
    while (i > 1) {
        i = i / 2;
        for (int j = 0; j < N; j++)
            count++;
    }
    return count;
}

3、如何推导出时间复杂度呢?有如下几个原则。

(1)如果运行时间是常数量级,则用常数1表示。

(2)只保留时间函数中的最高阶项。

(3)如果最高阶项存在,则省去最高阶项前面的系数。

举个栗子:T(n) = 3n,

最高阶项为3n,省去系数3,则转化的时间复杂度为:T(n)=O(n),这个栗子就是线性 O(n)。

再举个栗子:T(n) = 0.5n 2 + 0.5n,

最高阶项为0.5n ^2 ,省去系数0.5,则转化的时间复杂度为:T(n) =O(n^ 2 ) ,这个是平方o(n^2)。

三、空间复杂度

空间复杂度涉及的空间类型有:

输入空间: 存储输入数据所需的空间大小;
暂存空间: 算法运行过程中,存储所有中间变量和对象等数据所需的空间大小;
输出空间: 算法运行返回时,存储输出数据所需的空间大小;
通常情况下,空间复杂度指在输入数据大小为 NN 时,算法运行所使用的「暂存空间」+「输出空间」的总体大小。

根据不同来源,算法使用的内存空间分为三类: 

指令空间:

编译后,程序指令所使用的内存空间。

数据空间:

算法中的各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间

栈帧空间:
程序调用函数是基于栈实现的,函数在调用期间,占用常量大小的栈帧空间,直至返回后释放。如以下代码所示,在循环中调用函数,每轮调用 test() 返回后,栈帧空间已被释放,因此空间复杂度仍为 O(1) 。

根据从小到大排列,常见的算法空间复杂度有:

O(1)<O(logN)<O(N)<O(N2)<O(2N

1、空间复杂度种类

1.常数 O(1)O :

普通常量、变量、对象、元素数量与输入数据大小 NN 无关的集合,皆使用常数大小的空间。

void algorithm(int N) {
    int num = 0;
    int[] nums = new int[10000];
    Node node = new Node(0);
    Map<Integer, String> dic = new HashMap<>() {{ put(0, "0"); }};
}

2.线性 O(N) :

元素数量与 NN 呈线性关系的任意类型集合(常见于一维数组、链表、哈希表等),皆使用线性大小的空间。

void algorithm(int N) {
    int[] nums_1 = new int[N];
    int[] nums_2 = new int[N / 2];

    List<Node> nodes = new ArrayList<>();
    for (int i = 0; i < N; i++) {
        nodes.add(new Node(i));
    }

    Map<Integer, String> dic = new HashMap<>();
    for (int i = 0; i < N; i++) {
        dic.put(i, String.valueOf(i));
    }
}

3.平方 O(N^2) :

元素数量与 NN 呈平方关系的任意类型集合(常见于矩阵),皆使用平方大小的空间。

 

void algorithm(int N) {
    int num_matrix[][] = new int[N][N];

    List<List<Node>> node_matrix = new ArrayList<>();
    for (int i = 0; i < N; i++) {
        List<Node> nodes = new ArrayList<>();
        for (int j = 0; j < N; j++) {
            nodes.add(new Node(j));
        }
        node_matrix.add(nodes);
    }
}

 4.指数 O(2^N):

指数阶常见于二叉树、多叉树。例如,高度为 NN 的「满二叉树」的节点数量为 2^N ,占用 O(2^N) 大小的空间;同理,高度为 NN 的「满 mm 叉树」的节点数量为 m^N ,占用 O(m^N) = O(2^N)大小的空间。

 

二、时间与空间的取舍 

正所谓鱼和熊掌不可兼得,由于当代计算机的内存充足,通常情况下,算法设计中一般会采取「空间换时间」的做法,即牺牲部分计算机存储空间,来提升算法的运行速度。

方法一:暴力枚举
时间复杂度 O(N^2)
空间复杂度 O(1);属于「时间换空间」,虽然仅使用常数大小的额外空间,但运行速度过慢。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int size = nums.length;
        for (int i = 0; i < size - 1; i++) {
            for (int j = i + 1; j < size; j++) {
                if (nums[i] + nums[j] == target)
                    return new int[] { i, j };
            }
        }
        return new int[0];
    }
}

方法二:辅助哈希表
时间复杂度 O(N)O(N) ,空间复杂度 O(N)O(N) ;属于「空间换时间」,借助辅助哈希表 dic ,通过保存数组元素值与索引的映射来提升算法运行效率,是本题的最佳解法。

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int size = nums.length;
        Map<Integer, Integer> dic = new HashMap<>();
        for (int i = 0; i < size; i++) {
            if (dic.containsKey(target - nums[i])) {
                return new int[] { dic.get(target - nums[i]), i };
            }
            dic.put(nums[i], i);
        }
        return new int[0];
    }
}

ctrl+c ctrl+v 复制粘贴 - 设计师BGM专属表情包_设计师_美工_坏坏_动图_gif表情

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值