一、算法复杂度是什么?
- 算法复杂度分为时间复杂度和空间复杂度。
时间复杂度是指执行算法所需要的计算量(一段代码执行的次数多少透露出消耗的时间);
空间复杂度是指执行这个算法所需要的内存空间(一个算法在运行过程中临时占用存储空间大小的量度)。直接插入排序的空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。
问题1:直接插入排序的空间复杂度是O(1) ,为什么呢?
关于O(1)的问题, O(1)是说数据规模和临时变量数目无关,并不是说仅仅定义一个临时变量。举例:无论数据规模多大,我都定义100个变量,这就叫做数据规模和临时变量数目无关。就是说空间复杂度是O(1)。 也就是说: 当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);
问题2:排序算法的空间复杂度难道不都是O(n)吗?

看看网上这张图,空间复杂度并不是我想象的那样,对数据分配空间的大小呀o(╥﹏╥)o。那是怎样计算的呢?
啊啊啊,我是来说时间复杂度的呀!
问题3:稳定性又是如何判断?
排序前后两个相等的数相对位置不变,则算法稳定。那就看具体算法如何实现了。
啊啊啊,我是来说时间复杂度的呀!
二、时间复杂度如何计算?
1.可爱的人都会发现上面的表格中时间复杂度,分为三种,那就说说他们的符号表示:
Θ是平均时间复杂度(既是上界也是下界),O是最坏情况下的复杂度(表示上界),Ω是最好情况下的复杂度(表示下界)
2.从快速排序法来说说吧?
①手撕代码如下:
<script>
// 交换函数
function swap(arr, left, right) {
let temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
// 分(最终基准值)函数
function partiition(arr, left, right) {
let point;
// let point = arr[left]; 不优化默认为 左边第一个
//基准点的优化--取一个元素作为基准点
let m = left + parseInt((right - left) / 2);//不要忘了不是parseInt((right - left) / 2)哦,对于二分,右边要有偏移值哦,即+left
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
if (arr[m] > arr[right]) {
swap(arr, m, right);
}
if (arr[left] < arr[m]) {
swap(arr, left, m);
}
point = arr[left]; //保证基准点是 三个数的 中间大 的那个 ,降低时间复杂度(如果一个数组是升序或降序) -----哈哈哈 先把基准值存起来
while (left < right) {
while (left < right && arr[right] >= point) {//过滤比基准点大的
right--;
}
// swap(arr, left, right); // 小 交换
//无用交换的优化--赋值-------哈哈哈,把位置(右)空出来
arr[left] = arr[right];
while (left < right && arr[left] <= point) {
left++;
}
// swap(arr, left, right);
//无用交换的优化--赋值-------哈哈哈,把位置(左)空出来,把空位置(右)填上左边找的大的数
arr[right] = arr[left];
}
//无用交换的优化--记得赋值将基准值位置定下来(不会动了)-------哈哈哈,位置(此时左=右)空出来的基准值来凑
arr[left] = point;//left和right指针重合,即也可以 arr[right] = point;
return left; // 返回基准点位置
}
//快排
function quickSort(arr, left, right) {
let len = arr.length;
let point;
// 参数验证
left = typeof left != 'number' ? 0 : left;
right = typeof right != 'number' ? len - 1 : right;
// 递归终止条件
if (left >= right) return;
// 重要部分
point = partiition(arr, left, right);
quickSort(arr, left, point - 1);
quickSort(arr, point + 1, right);
return arr;
}
// 随机生成 长度为len且值为任意值(正、0、负)的数组
function generateRandomArr(len) {
let arr = [];
for (i = 0; i < len; i++) {
let item = Math.round(Math.random() * 1000)-500;//[-500,500]
arr.push(item);
}
return arr;
}
let len = Math.ceil(Math.random() * 100);//len>0 ,意味着数组非空
console.log(quickSort(generateRandomArr(len)));
</script>
②快排空间复杂度不稳定,怎么说呢?
是因为不稳定发生在基准值ponit和arr[left==right] 交换的时刻,对应代码就是 【arr[left] = point;//left和right指针重合,即也可以 arr[right] = point;】,就是在基准值和arrleft==right]交换的时候,很有可能把前面的元素的稳定性打乱,比如序列为 5, 3, 1 ,1, 8 ,1,6,7,现在基准值5(第1个元素)和1(第6个元素计)交换就会把元素1的稳定性打乱。
③快排的最坏复杂度怎么说呢?
比如 一个数组已经是正序或逆序-----时间复杂度都是O(n²)
如何理解O(n²)呢?
对于正序:比如 1,2,3,4,5-->快排每趟确定一个基准,依次确定是 1,2,3,4,5 ,范围变化【1,【2,【3,【4,【5】】】】】 所以是n*n
对于逆序:比如5,4,3,2,1-->快排每趟确定一个基准, 依次确定是5,1,4,2,3 ,范围变化【【4,3,2,1】,5】这个不太好用中括号一次性表示,明白意思就行 所以是n*n
注:每趟:指跑遍数据一圈,导致其分治思想一边倒,从而时间复杂度太高。
④分治法是什么?
分治法其实就是一种策略。具体一下理解:
分:一个问题分成子问题。---------------------------对应快排:意思就是将数组分为两个数组
治:递归的解决每个子问题。------------------------对应快排:意思就是每个子数组的排序
合:将每个子问题的解合并成大问题的解。------对应快排:意思就是合并两个有序的数组(表象)
⑤快排的时间复杂度是多少呢?是O(nlogn)又是怎么计算的呢?
可以用快排时间复杂度的递推关系式:T(n)=2⋅T(n/2)+O(n) ,啊,补充一下“主定理”知识点
⑥主定理是什么?
递推关系式 ,其中 为问题规模, 为地递推的子问题数目, 为每个子问题的规模(假设每个子问题的规模基本一样), 为递推以外(就是分、治、合三个中不包括分)进行的计算量(也就是分治的运算时间)。其中f(n)满足:
所以,对于快排,有先分为两个子数组,也就是两个子问题规模,所以a=2,每个子问题规模,也就是每个数组在相等(或差不多相等)情况下都看做n/2个数,所有n/b=n/2,还有附加计算量:分(递归不算)、治(排序)为 n 、合(已经是左边小右边大不用手动合并,也不算)所以f(n)=O(n) 所以快排时间复杂度的递推关系式:T(n)=2⋅T(n/2)+O(n),又因为O(n)==n,所以快排时间复杂度=O(nlogn)。
⑦logn是什么意思?
见过lgn代表以10为底,lnn代表以e为底,那logn是什么呢?
这个就要用到我们高一学的换底公式:loga^b=logc^b/logc^a,所以比如时间复杂度t=log2^n=log8^n/log8^2,又因为计算时间复杂度是针对一个大大大数组,常数项可以忽略,所以t=log2^n=log8^n,那么你发现有没有底数都无所谓,所以logn就是底数无所谓取就行。
时间复杂度总算是计算出来了。
⑧上面用的主定理,下面我们用通项公式计算一下(做个高中题吧)
⑨那我问用递归树分析一下(用一下高中的极限思想),计算时间复杂度。
想想看,时间复杂度就是这么一回事。
⑩ 最后分治法中最重要的还是递归哦。