算法
算法(Algorithm): 是对特定问题求解步骤的一种描述,它是指令的有序序列
,其中每一条指令
表示一个或者多个操作
。
5个重要特征
有穷性
执行有穷步骤后结束,有穷时间内完成
确定性
每一条指令有确切含义,任何条件下只有唯一的执行路径,相同的输入相同的输出
可行性
一个算法是能行的, 描述的步骤基于已实现的基本运算
执行有限次
实现
输入
有0个或者多个输入,刻画运算的对象的初始情况
输出
有一个个或者多个输出, 这些输出和输入存在特定关系
算法设计的要求:
- 正确性(语法正确、对于一切合法无论简单或苛刻的输入数据都可以得到满足规格说明要求的结果)
- 可读性(容易理解、交流、调试和修改,没有隐藏错误易)
- 健壮性(对非法输入不会产生莫名其妙的输出结果)
- 效率与低存储量需求,
效率指的是算法执行时间
,存储量指的是算法执行过程中需要的最大存储空间
.
算法效率的度量
事后统计法
通过设计好的测试程序和数据
,利用计算机计时器对不同算法编制的程序
的运行时间进行比较,从而确定算法效率的高低.
缺陷:
- 必须依据算法实现编制好测试程序
- 所得时间的统计依赖计算软硬件环境因素。
事前分析估算
- 算法采用的策略和方案
- 问题的规模, n = 1000, n = 10000;
- 书写的语言,语言级别越高,执行效率越低
- 编译程序所产生的代码的质量
- 机器执行指令的速度。
由此可见,抛开这些与计算机硬件,软件有关的因素,一个程序的运行时间依赖于算法的好坏和问题的输入规模.(所谓的问题输入规模是指输入量的多少(通常用整数量n 表示))
一个算法是有控制结构(顺序,分支,循坏)和原操作(固定数据类型的操作),算法时间取决于两者的综合效果。
算法时间度量:
对所研究问题(算法类型)基本操作的原操作以及对基本操作重复执行的次数度量。算法的基本操作的重复次数是问题规模
的某个函数f(n)
. 算法时间量度记做
T(n) = O(f(n));
时间复杂度:
随着问题规模的n 的增大,算法执行时间的增长率和f(n)的增长率相同。
T(n) = O(f(n));
频度
某些控制语句的重复的执行的次数,(for, while, do while)。
问题的基本的原操作重复执行次数和算法的执行时间成正比。
常见的时间复杂度:
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!);
算法平均复杂度
对所有可能的输入数据集合的期望值
最坏时间复杂度。
分析最坏情况估算执行时间的上线
...
for(i =2; i <=n; i++){
for(j =2; j <=i-1; j++){
++x;
a[i][j] = x;
}
}
语句频度:(n - 1)(n-2)/2
时间平均复杂度T arg(n) = O(n^2);
最坏时间复杂度T(n) = O(n^2);
算法空间复杂度
S(n) = O(f(n));
n 问题的规模
f(n)为语句关于n所占存储空间的函数。 一个上机程序除了需哟啊存储空间寄存本身所用的指令、常数、变量和输入数据外,对数据进行操作的工作单元和存储一些为实现计算所需要的信息和辅助空间
.
排序算法
通过特定的算法因式
将一组或多组数据
按照既定模式
进行重新排序
。
评价标准
- 稳定性:
当
两个相同的元素
同时出现于某个序列之中,则经过一定的排序算法之后,两者在排序前后的相对位置不发生变化
.
注:稳定性是一个特别重要的评估标准。稳定的算法在排序的过程中不会改变元素彼此的位置的相对次序,反之不稳定的排序算法经常会改变这个次序,这是我们不愿意看到的
- 时间复杂度
- 空间复杂度
排序算法的分类
非线性时间比较类排序:比较,由时间复杂度不能突破(nlogn)
交换排序(冒泡、快速)、插入排序(直接插入、希尔排序)、选择排序(简单选择排序,堆排序),
归并排序(二路归并,多路归并)
线性时间非比较类排序:不比较,突破基于比较排序的时间下界,以线性时间运行 基数排序、桶排序、计数。
内部排序:指的是待排序记录存放在计算机随机存储器进行排序
外部排序:待排序记录数量的数量很大,一次不可以存放全部记录,需对外存访问
快速排序(Quicksort)
通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行递归排序,以达到整个序列有序。 这个关键字是从数列中挑出一个元素,也称为 “基准”(pivot).
实现步骤:
- 选择一个基准元素
target
(一般选择第一个数) - 将比
target
小的元素移动到数组左边,比target
大的元素移动到数组右边 - 分别对
target
左侧和右侧的元素进行快速排序
复杂度
时间复杂度:平均O(nlogn)
,最坏O(n2)
,实际上大多数情况下小于O(nlogn)
空间复杂度:O(logn)
(递归调用消耗)
稳定性
不稳定
写法1 左开右闭
记录一个索引l
从数组最左侧开始,记录一个索引r
从数组右侧开始
在l<r
的条件下,找到右侧小于target
的值array[r]
,并将其赋值到array[l]
在l<r
的条件下,找到左侧大于target
的值array[l]
,并将其赋值到array[r]
这样让l=r
时,左侧的值全部小于target
,右侧的值全部小于target
const quickSort = function(array, start , end) {
if(end - start < 1) return;
let l = start;
let r = end;
const target = array[l];
while(l < r) {
while(l < r && array[r] >= target) {
r--
};
array[l] = array[r];
while(l < r && array[l] < target) {
l++
};
array[r] = array[l];
}
array[l] = target
quickSort(array, start, l - 1);
quickSort(array, l+1, end);
}
const arr = [4, 1, 5, 7, 3, 9];
quickSort(arr, 0, 5);
写法2
单独开辟两个存储空间left
和right
来存储每次递归比target
小和大的序列
每次递归直接返回left、target、right
拼接后的数组
浪费大量存储空间,写法简单
function quickSort(array) {
if(array.length < 2) {
return array;
}
const target = array[0];
const left = [];
const right = [];
for(let i = 1; i < array.length; i++) {
if(array[i] < target) {
left.push(array[i])
} else {
right.push(array[i]);
}
}
return quickSort(left).concat([target], quickSort(right));
}
const arr = [4, 1, 5, 7, 3, 9];
quickSort(arr, 0, 5);
插入排序
将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。
插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
复杂度
时间复杂度:O(n2)
空间复杂度:O(1)
稳定性
稳定
function insertSort(array) {
for(let i = 1, len = array.length; i < len; i++) {
let target = i;
for(let j = i - 1; j >=0; j--) {
if(array[target] < array[j]) {
[array[target], array[j]] = [array[j], array[target]];
target = j;
}else break;
}
}
}
const arr = [4, 1, 5, 7, 3, 9];
insertSort(arr, 0, 5);
堆排序
创建一个大顶堆,大顶堆的堆顶一定是最大的元素。
交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。
从后往前以此和第一个元素交换并重新构建,排序完成。
复杂度
时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性
不稳定
function heapSort(array) {
creatHeap(array);
for(let i = array.length - 1; i > 0; i--) {
[array[i], array[0]] = [array[0], array[i]];
adjust(array, 0, i);
}
return array;
}
function creatHeap(array) {
const len = array.length;
const start = parseInt(len/2) - 1;
for(let i = start; i >=0; i--) {
adjust(array, i, len);
}
}
// 将第target个元素进行下沉,孩子节点有比他大的就下沉
function adjust(array, target, len) {
for(let i = 2 * target + 1; i< len; i = 2 * i + 1) {
// 找到孩子节点中最大的
if(i + 1 < len && array[i + 1] > array[i]) {
i = i + 1;
}
// 下沉
if(array[i] > array[target]){
[array[i], array[target]] = [array[target], array[i]]
}else {
break;}
}
}
const arr = [4, 1, 5, 7, 3, 9];
heapSort(arr);
归并排序
利用归并
的思想实现的排序方法。
该算法是采用分治法(Divide and Conquer
)的一个非常典型的应用。(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
- 若将两个有序表合并成一个有序表,称为二路归并。
分割:
- 将数组从中点进行分割,分为左、右两个数组
- 递归分割左、右数组,直到数组长度小于
2
归并
需要合并,那么左右两数组已经有序了。
创建一个临时存储数组temp
,比较两数组第一个元素,将较小的元素加入临时数组
若左右数组有一个为空,那么此时另一个数组一定大于temp
中的所有元素,直接将其所有元素加入temp
-
时间复杂度:
O(nlogn)
-
空间复杂度:
O(n)
-
稳定
解法一
分割数组时直接将数组分割为两个数组,合并时直接合并数组。 缺点:空间复杂度略高,需要复制多个数组
function mergeSort(array) {
if(array.length < 2) {
return array;
}
const mid = array.length >> 1;
const front = array.slice(0, mid);
const end = array.slice(mid);
return merge(mergeSort(front), mergeSort(end));
}
function merge(front, end) {
const temp = [];
while(front.length && end.length) {
if(front[0] < end[0]) {
temp.push(front.shift())
} else {
temp.push(end.shift())
}
}
while(front.length) {
temp.push(front.shift());
}
while(end.length) {
temp.push(end.shift())
}
return temp;
}
const arr = [4, 1, 5, 7, 3, 9];
mergeSort(arr);
解法2
记录数组的索引
,使用left、right
两个索引来限定当前分割的数组。
优点:空间复杂度低,只需一个temp
存储空间,不需要拷贝数组
function mergeSort(array, left, right, temp) {
if (left < right) {
const mid = Math.floor((left + right) / 2);
mergeSort(array, left, mid, temp)
mergeSort(array, mid + 1, right, temp)
merge(array, left, right, temp);
}
return array;
}
function merge(array, left, right, temp) {
const mid = Math.floor((left + right) / 2);
let leftIndex = left;
let rightIndex = mid + 1;
let tempIndex = 0;
while (leftIndex <= mid && rightIndex <= right) {
if (array[leftIndex] < array[rightIndex]) {
temp[tempIndex++] = array[leftIndex++]
} else {
temp[tempIndex++] = array[rightIndex++]
}
}
while (leftIndex <= mid) {
temp[tempIndex++] = array[leftIndex++]
}
while (rightIndex <= right) {
temp[tempIndex++] = array[rightIndex++]
}
tempIndex = 0;
for (let i = left; i <= right; i++) {
array[i] = temp[tempIndex++];
}
}
const arr = [4, 1, 5, 7, 3, 9];
mergeSort(arr, 0, 5, []);
冒泡
循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。
这样一次循环之后最后一个数就是本数组最大的数。
下一次循环继续上面的操作,不循环已经排序好的数。
优化:当一次循环没有发生冒泡,说明已经排序完成,停止循环。
-
时间复杂度:
O(n2)
-
空间复杂度:
O(1)
-
稳定
function bubbleSort(array) {
for(let j = 0, len = array.length; j < len; j++) {
let complete = true;
for(let i = 0; i < len - 1 - j; i++) {
if(array[i] > array[i+1]) {
[array[i], array[i+1]] = [array[i+1], array[i]];
complete = false;
}
}
if(complete) {
break;
}
}
return array;
}
const arr = [4, 1, 5, 7, 3, 9];
bubbleSort(arr);
选择排序
每次循环选取一个最小的数字放到前面的有序序列中。
-
时间复杂度:
O(n2)
-
空间复杂度:
O(1)
-
不稳定
function selectionSort(array) {
for(let i = 0, len = array.length; i < len -1; i++) {
let minIndex = i;
for(let j = i+1; j < len; j++) {
if(array[j] < array[minIndex]) {
minIndex = j
}
}
[array[minIndex], array[i]] = [array[i], array[minIndex]];
}
}
const arr = [4, 1, 5, 7, 3, 9];
selectionSort(arr);