数据结构与算法
学习数据结构与算法之美的小结
是什么:
数据结构指的是"一组数据的存储结构",算法指的是"操作数据的一组方法".
数据结构是为算法服务的,算法是要作用再特定的数据结构上的.
学什么:
- 效率和资源消耗的度量衡–复杂度分析.
- 最常用、最基础的20个数据结构与算法,学习他们的:“来历”、“特点”、“适合解决什么问题"和"实际的应用场景”.
数据结构:数组、栈、队列、链表、散列表、二叉树、堆、跳表、图、Tire树
算法: 递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法
为什么学
- 直接好处是能够有写出性能更优的代码.
- 算法,是一种解决问题的思路和方法,有机会应用到生活和事业的其他方面.
- 长期来看,大脑思考能力是个人最重要的核心竞争力,而算法是为数不多的能够有效训练大脑
复杂度分析
什么是复杂度分析?
- 数据结构和算法解决是"如何让计算机更快时间、更省空间的解决问题".
复杂度分析从执行时间和占用空间两个维度来评估数据结构和算法的性能. - 分别用时间复杂度和空间复杂度两个概念来描述性能问题,二者统称为复杂度.
时间复杂度(渐进时间复杂度):表示算法的执行时间与数据规模之间的增长关系
空间复杂度(渐进空间复杂度):表示算法使用的存储空间与数据规模之间的增长关系 - 复杂度描述的是算法执行时间(或占用空间)与数据规模的增长关系.
为什么要进行复杂度分析?
- 和性能测试相比,复杂度分析有不依赖执行环境、成本低、效率高、易操作、指导性强的特点
- 掌握复杂度分析,将能编写出性能更优的代码,有利于降低系统开发和维护成本
- 复杂度分析可以粗略的估计算法的执行效率, 对程序的运行速度有一个感性的认识
如何进行复杂度分析?
- 大O表示法
- 来源
算法的执行时间与每行代码的执行次数成正比,用T(n) = O(f(n))表示,其中T(n)表示算法执行总时间,f(n)表示每行代码执行总次数,而n往往表示数据的规模. - 特点
以时间复杂度为例,由于时间复杂度描述的是算法执行时间与数据规模的增长变化趋势,所以常量阶、低阶以及系数实际上对这种增长趋势不产决定性影响,所以在做时间复杂度分析时忽略这些项.
- 来源
- 复杂度分析法则
- 单段代码看高频:比如循环.
- 多段代码取最大:比如一段代码中有单循环和多重循环,那么取多重循环的复杂度.
- 嵌套代码求乘积:比如递归、多重循环等
- 多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加.
常用的复杂度级别?
多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长.
1. O(1)(常数阶): 哈希查找
2. O(logn)(对数阶): 二分查找
3. O(n)(线性阶): 计数排序、基数排序、桶排序
4. O(nlogn)(线性对数阶): 归并排序、快速排序
5. O(n^2^)(平方阶): 冒泡排序、插入排序、选择排序
6. O(n^3^)(立方阶)
非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差.
1. O(2^n^)(指数阶)
2. O(n!)(阶乘阶)
更精确的估算: 时间复杂度的系数、常数 、低阶
实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来
比较次数和交换(或移动)次数
排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动.所以,应该把比较次数和交换(或移动)次数也考虑进去.
如何掌握好复杂度分析方法?
多练, 动手写, 实际用
衡量复杂度的级别
- 最坏情况时间复杂度:代码在最理想情况下执行的时间复杂度.
- 最好情况时间复杂度:代码在最坏情况下执行的时间复杂度.
- 平均时间复杂度:用代码在所有情况下执行的次数的加权平均值表示.
代码在不同情况下复杂度出现量级差别,则用代码所有可能情况下执行次数的加权平均值表示 - 均摊时间复杂度:在代码执行的所有复杂度情况中绝大部分是低级别的复杂度,个别情况是高级别复杂度且发生具有时序关系时,可以将个别高级别复杂度均摊到低级别复杂度上.基本上均摊结果就等于低级别复杂度
时间复杂度分析
// 往数组中添加一个元素
void add(int element) {
if (i >= len) {
int new_array[] = new int[len*2];
for (int j = 0; j < len; ++j) {
new_array[j] = array[j];
}
array = new_array;
len = 2 * len;
}
array[i] = element;
++i;
}
- 最好时间复杂度: 跳过if判断, 进行赋值和自增两步操作. 为O(1)
- 最坏时间复杂度: 申请新空间并进行copy操作. O(n)
- 平均复杂度: (1+1+…+1+n)/(n+1) = 2n/(n+1) 为O(1);
- 加权平均时间复杂度: 1*(1/n+1)+1*(1/n+1)+…+1*(1/n+1)+n*(1/(n+1))=1, 为O(1);
- 均摊时间复杂度: 前n个操作复杂度都是O(1),第n+1次操作的复杂度是O(n),所以把最后一次的复杂度分摊到前n次上,那么均摊下来每次操作的复杂度为O(1)
空间复杂度
算法的内存消耗可以通过空间复杂度来衡量.
原地排序算法:就是特指空间复杂度是 O(1) 的排序算法
逆序数
原始数据的不同的有序程度,对应不同的时间复杂度.
数据的有序程度可以用*逆序数*表示.
在一个排列中,如果前面的数大于后面的数,就称为一个逆序.一个排列中逆序的总数就称为这个排列的逆序数.
{2,4,3,1}中,逆序依次为 (2,1),(4,3),(4,1),(3,1),该序列的逆序数为4
**有序度**: 数组中具有有序关系的元素对的个数
**有序元素对**:a[i] <= a[j], 如果 i < j
**满有序度**:完全有序的数组的有序度.
6,5,4,3,2,1: 0+0+0+0+0+0
1,2,3,4,5,6: 5+4+3+2+1+0, n(n-1)/2取n=6得15
**逆序度 = 满有序度 - 有序度**
稳定性
概念:待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变.
实际上,双十一时的订单都会有两个属性,下单时间和金额.
如何计算发货顺序呢,我们先按照下单时间给订单排序,排序完成之后,我们用稳定排序算法,按照订单金额重新排序.
两遍排序之后,我们得到的订单数据就是按照金额从小到大排序,
金额相同的订单按照下单时间从早到晚排序,稳定排序算法可以保持金额相同的两个对象,在排序之后的前后顺序不变.
综合分析
Bubble Sort 冒泡排序
- 原地排序算法: 只涉及相邻数据的交换, 需要一个常量级的临时空间, 空间复杂度为O(1)
- 稳定的: 相等的元素不会进行交换,所以等值的元素在排序前后不会改变顺序
- 时间复杂度:
- 最好时只要进行一趟冒泡, O(n)
- 最坏情况要进行n趟冒泡,O(n^2)
- 平均情况作简单估算,
- 冒泡排序包含比较和交换两个操作, 比较只需读值不需要写内存, 所以我们考虑交换操作,交换一次, 逆序度就减一.
- 逆序度=满有序度-有序度, 所以逆序度一定小于满有序, 也就是n(n-1)/2,
逆序度为0时, 不需进行交换操作,取两者中间值n(n-1)/4
def bubbleSort(arr):
length = len(arr)
if length <= 0:
return arr
for i in range(length):
# 每一趟冒泡都会通过交换,排好一个元素, 当没有交换操作时就意味着整个数组是有序的
isCompare = False
for j in range(length-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
isCompare = True
if not isCompare:
break
return arr
算法书籍
- 易: 大话数据结构,算法图解
- 面试: 剑指 offer,编程珠玑,编程之美
- 常: 数据结构与算法分析, 数据结构与算法JavaScript描述, 数据结构与算法Python描述
- 闲: 算法帝国,数学之美,算法之美
- 难: 算法导论, 算法, 计算机程序设计艺术