复杂度
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度: 运行完一个程序所需内存的大小。
- 常见的时间复杂度量级有:
- 常数阶O(1)
- 对数阶O(logN)
- 线性阶O(n)
- 线性对数阶O(nlogN)
- 平方阶O(n²)
- 立方阶O(n³)
- K次方阶O(n^k)
- 指数阶(2^n)
上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。
举例说明:
//常数阶O(1): 只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
//线性阶O(n): 只有一层循环, 里面的代码会执行n遍, 那这个代码的时间复杂度就都是O(n)
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
//对数阶O(logN):只有一层循环,且循环 log2^n 次, 那这个代码的时间复杂度就都是O(logN)
int i = 1;
while(i<n)
{
i = i * 2;
}
//线性对数阶O(nlogN): 将上面两种情况结合一下,也就是将 O(logN)的代码循环N遍,时间复杂度就是O(nlogN)
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
//平方阶O(n²): 嵌套了2层n循环, 时间复杂度就是O(n²),同理,3层n循环就是立方阶O(n³)、k层循环就是K次方阶O(n^k)
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
- 常见的空间复杂度有:
- 常数阶O(1)
- 线性阶O(n)
- O(n²)
举例说明:
//常数阶O(1): 算法执行所需要的临时空间不随着某个变量n的大小而变化,始终是一个常数,那这个代码的空间复杂度就都是O(1)
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
//线性阶O(n): 只占用了一个长度为n的内存空间,且随着某个变量n的大小而变化,那这个代码的空间复杂度就都是O(n)
int[] m = new int[n]
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
//平方阶O(n^2):二维数组的空间复杂度为O(n^2)
int m = new int[n][n]
for(x=1; i<=n; x++)
{
j = i;
j++;
}
排序
-
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
-
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
-
内排序:所有排序操作都在内存中完成;
-
外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
常见的排序算法有:
-
快速排序 选择一个目标值,比目标值小的放左边,比目标值大的放右边,目标值的位置已排好,将左右两侧再进行快排。
-
归并排序 将大序列二分成小序列,将小序列排序后再将排序后的小序列归并成大序列。
-
选择排序 每次排序取一个最大或最小的数字放到前面的有序序列中。
-
插入排序 将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
-
冒泡排序 循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。下一次循环继续上面的操作,不循环已经排序好的数。
-
堆排序 创建一个大顶堆,大顶堆的堆顶一定是最大的元素。交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。从后往前以此和第一个元素交换并重新构建,排序完成。
快速排序
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。
步骤(利用了分治的思想):
- 选择一个基准元素target(一般选择第一个数)
- 将比target小的元素移动到数组左边,比target大的元素移动到数组右边
- 分别对target左侧和右侧的元素进行快速排序
写法一,时间复杂度O(n)、空间复杂度:O(n),不稳定:
function quickSort(arr) {
if (arr.length < 2) {
return arr
}
let target = arr[0]
let left = []
let right = []
for (let i = 1; i < arr.length; i++) {
if (target < arr[i]) {
right.push(arr[i])
} else {
left.push(arr[i])
}
}
return quickSort(left).concat([target]).concat(quickSort(right))
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(quickSort(arr))
写法二,时间复杂度:平均O(nlogn),最坏O(n2),实际上大多数情况下小于O(nlogn), 空间复杂度:O(logn):
function quickSort(arr, left, right) {
if (left + 1 > right) return
let target = arr[right]
let l = left
let r = right
while (l < r) {
while (l < r && arr[l] < target) {
l++
}
arr[r] = arr[l]
while (l < r && arr[r] > target) {
r--
}
arr[l] = arr[r]
}
arr[l] = target
quickSort(arr, left, l - 1)
quickSort(arr, l + 1, right)
return arr
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(quickSort(arr, 0, arr.length - 1))
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
将两个已排好序的数组合并成一个有序的数组,称之为归并排序。
折分:
- 将数组从中点进行分割,分为左、右两个数组
- 递归分割左、右数组,直到数组长度小于2
合并:
- 如果需要合并,那么左右两数组已经有序了。
- 创建一个临时存储数组temp,比较两数组第一个元素,将较小的元素加入临时数组,若左右数组有一个为空,那么此时另一个数组一定大于temp中的所有元素,直接将其所有元素加入temp
时间复杂度O(nlogN),空间复杂度O(n),稳定:
function mergeSort(arr) {
if (arr.length < 2) {
return arr
}
const mid = Math.floor(arr.length / 2)
const left = arr.slice(0, mid)
const right = arr.slice(mid)
return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
let result = []
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift())
} else {
result.push(right.shift())
}
}
while (left.length) {
result.push(left.shift())
}
while (right.length) {
result.push(right.shift())
}
return result
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(mergeSort(arr))
// [0,1,2,3,4,5,6,7,8,9]
选择排序
每次循环选取一个最小的数字放到前面的有序序列中。
步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
时间复杂度O(n^2),空间复杂度O(1),不稳定:
function selectSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let minIndex = i
//找到剩下的最小值
for (let j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
minIndex = j
}
}
//把此轮最小值换到前面
[arr[minIndex], arr[i]] = [arr[i], arr[minIndex]]
}
return arr
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(selectSort(arr))
// [0,1,2,3,4,5,6,7,8,9]
插入排序
步骤:
- 将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。
- 插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。
时间复杂度O(n^2),空间复杂度O(1),稳定:
function insertSort(arr) {
for (let i = 1; i < arr.length; i++) {
let target = i //插入的元素
for (let j = i - 1; j >= 0; j--) {
//从右向左比较,如果插入的元素更小,则交换并继续向左比较
if (arr[target] < arr[j]) {
[arr[target], arr[j]] = [arr[j], arr[target]]
target = j
} else {
break
}
}
}
return arr
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(insertSort(arr))
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。 走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
-
什么时候最快 当输入的数据已经是正序时。
-
什么时候最慢 当输入的数据是反序时。
步骤:
- 循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。
- 这样一次循环之后最后一个数就是本数组最大的数。下一次循环继续上面的操作,不循环已经排序好的数。
- 当一次循环没有发生冒泡,说明已经排序完成,停止循环。
时间复杂度O(n^2),空间复杂度O(1),稳定:
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let finished = true
//每i轮后,第 (arr.length - 1 - i)后的元素都是有序的了
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
finished = false
}
}
if (finished) {
break
}
}
return arr
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
console.log(bubbleSort(arr))
堆排序
步骤:
- 创建一个大顶堆,大顶堆的堆顶一定是最大的元素。
- 交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。
- 从后往前以此和第一个元素交换并重新构建,排序完成。
时间复杂度O(nlogN),空间复杂度O(1),不稳定:
function buildMaxHeap(arr) {
if (!Array.isArray(arr)) return []
//将null插到数组第一个位置上
arr.unshift(null)
let lastParentIndex = Math.floor((arr.length - 1) / 2)
for (let i = lastParentIndex; i > 0; i--) {
maxHeapify(arr, i, arr.length - 1)
}
arr.shift()
return arr
}
function maxHeapify(arr, i, size) {
let left = 2 * i
let right = left + 1
//左右孩子中较大的一个
let maxlr = -1
//无左右孩子节点
if (left > size && right > size) {
return
}
//只有左孩子节点
if (left <= size && right > size) {
maxlr = left
}
//只有右孩子节点
if (right <= size && left > size) {
maxlr = right
}
//同时有左右孩子节点
if (left <= size && right <= size) {
maxlr = arr[left] < arr[right] ? right : left
}
if (arr[i] < arr[maxlr]) {
swap(arr, i, maxlr)
maxHeapify(arr, maxlr, size)
}
}
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function heapSort(arr) {
arr[0] !== null && arr.unshift(null)
for (let j = arr.length - 1; j > 0; j--) {
//将堆顶与堆底元素交换,分离出最大的元素
swap(arr, 1, j)
//重新调整大顶堆
maxHeapify(arr, 1, j - 1)
}
arr.shift()
return arr
}
var arr = [3, 1, 8, 4, 6, 0, 2, 9, 5, 7]
buildMaxHeap(arr)
console.log(heapSort(arr))
记忆
冒泡、选择、插入需要两个for循环,每次只关注一个元素,平均时间复杂度为O(n²))(一遍找元素O(n),一遍找位置O(n))快速、归并、堆基于二分思想,log以2为底,平均时间复杂度为O(nlogN)(一遍找元素O(n),一遍找位置O(logN))冒泡、插入、归并排序是稳定的,快速、选择、堆排序不稳定的。