声明:根据左程云老师的网课所作笔记,代码是自己根据课程理解自己书写的
如果有错误非常感谢指出
数据结构与算法
提示:以下是本篇文章正文内容,代码示例可供参考。
第一章、算法基础
1.1 算法概念
算法(Algorithm): 一个计算过程,解决问题的办法
Niklaus Wirth: ‘程序 = 数据结构 + 算法 ’
数据结构(静态),算法(动态)
1.2 时间复杂度
1.2.1 概念
时间复杂度:用来评估算法运行效率的一个式子。(单位)
一般来说,时间复杂度高的算法比复杂度低的算法慢。
常数时间的操作:
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的指标。常用O(读作big O)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法中发生了多少常数操作,进而总结出常数操作数量的表达式。
在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为 f(N),那么时间复杂度为 O(f(N))。
评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下实际运行时间,也就是“常数项时间”。
常见的时间复杂度(按效率排序)
o(1) < o(log n) < o(n) < o(n*logn) < o(
n
2
n^{2}
n2) < o(
n
2
n^{2}
n2logn) < o(
n
3
n^{3}
n3)
复杂问题的时间复杂度
o(n!) o(
n
2
n^{2}
n2) o(
n
n
n^{n}
nn)
1.2.1 如何快速的判断算法的复杂度
(适合绝大多数简单的情况)
1、确定问题规模n
2、循环减半过程———>logn
3、k层关于n的循环——>
n
k
n^{k}
nk
1.3 空间复杂度
1.3.1 概念
空间复杂度:用来评估算法内存占用大小的式子
1.3.2 表示方式
空间复杂度的表示方式与时间复杂度的完全一样
1、算法使用了几个变量:o(1)
2、算法使用了长度为n的一维列表:o(n)
3、算法使用了m行n列的二维列表:o(mn)
时间比空间重要 —>空间换时间
1.4 对数器
1、有一个你像测的方法a
2、实现复杂度不好但是容易实现的方法b
3、实现一个随机样本产生器
4、把方法a和方法b跑相同的随机样本,看看得到的结果是否一样
5、如果有一个随机样本使得对比结果不一致,打印样本进行人工干预,该方法a或者方法b
6、当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
1.5 比较器
1):比较器的实质就是重载比较运算符
2):比较器可以很好的应用在特殊的标准的排序上
3):比较器可以很好的应用在根据特殊标准排序的结构上
返回负数时,第一个参数排在前面
返回正数时,第二个参数排在前面
返回0时,谁在前面无所谓
不基于比较的排序都要根据数据情况定制
第二章、常见的算法精讲
2.1 选择排序
2.1.1 选择排序原理
选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,继续放在起始位置知道未排序元素个数为0。
有一个长度为n的数组,首先我们在 0——n-1的位置上遍历一遍,找到一个最小值,把这个最小值与0位置上的值进行交换,然后再在1——n-1的位置上找到一个最小值,将这个最小值与1位置上的值进行交换。依次类推2——n-1、3——n-1、4——n-1…n-1——n-1。这样我们就将数组n个位置上的数按从小到大的顺序进行排列下来了。
2.1.2 选择排序时间复杂度分析
时间复杂度分析就是计算我们这个算法中有多少个常数操作。
1、最开始 从0——N-1的位置上:
都需要看一眼(数组寻址一下:常数操作)。看: N次
都需要跟我们选出来的目前最小的数比较一下(常数操作)。比较: N次
最后将我们选出的最小值与0位置上的数进行交换常数操作。 交换:1次
2、从1——N-1的位置上:
都需要看一眼(数组寻址一下:常数操作)。看: N-1次
都需要跟我们选出来的目前最小的数比较一下(常数操作)。比较: N-1次
最后将我们选出的最小值与0位置上的数进行交换常数操作。 交换:1次
3、从2——N-1的位置上:
都需要看一眼(数组寻址一下:常数操作)。看: N-2次
都需要跟我们选出来的目前最小的数比较一下(常数操作)。比较: N-2次
最后将我们选出的最小值与0位置上的数进行交换常数操作。 交换:1次
:
:
:
(依次类推)
N、从N-1——N-1的位置上:
都需要看一眼(数组寻址一下:常数操作)。看:1次
都需要跟我们选出来的目前最小的数比较一下(常数操作)。比较: 1次
最后将我们选出的最小值与0位置上的数进行交换常数操作。 交换:1次
总结下:
总共看了:
N+(N-1)+(N-2)+(N-3)…+3+2+1
总共比较了:
N+(N-1)+(N-2)+(N-3)…+3+2+1
总共交换了:
N
其中总结和比较的次数分别是 两个等差数列
将这些加到一起:
可以总结为: a
N
2
N^{2}
N2 + bN + c 个常数操作。
(去掉低阶部分和高阶的系数)故他的时间复杂度为 O(
N
2
N^{2}
N2)
2.1.3 选择排序代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>选择排序</title>
</head>
<body>
<script>
function selectionSort(arr) {
if (arr == null || arr.length < 2){
return;
}
for (let i = 0; i < arr.length - 1; i++){
let minIndex = arr[i];
for (let j = i + 1; j < arr.length; j++){
minIndex = arr[j] < arr[minIndex] ? j : minIndex
}
swap(arr, i, minIndex);
}
return arr
};
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1,4,2,3,4,2,5,8];
console.log(selectionSort(array))
</script>
</body>
</html>
2.2 冒泡排序
2.2.1 冒泡排序原理
冒泡排序是比较基础的排序算法之一,其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
比如: 对于一个数组: 2 3 5 4 3 6
1、 从0 ~ 5 位置上依次两两比较, 将大的像右移;
0 和 1 位置上比较 3比2大 不用换
1 和 2 位置上比较 5比3大不用换
2 和 3 位置上比较5比4大 5和4交换 ————>数组变成: 2 3 4 5 3 6
3 和 4 位置上比较5比3大 5和3交换 ————>数组变成: 2 3 4 3 5 6
4 和 5 位置上比较 6比5大不用换
这次比较下来 我们搞定了 数组的最后一个位置(5位置)的数为最大值
2、 从0 ~ 4 位置上依次两两比较, 将大的像右移;————> 搞定4位置上的数。
3、 从0 ~ 3 位置上依次两两比较, 将大的像右移;————> 搞定3位置上的数。
:
:
:
终实现整个数组从大到小排列。
2.2.2 冒泡排序代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>冒泡排序</title>
</head>
<body>
<script>
function bubbleSorting(arr) {
if (arr == null || arr.length < 2){
return;
}
for (let i = arr.length - 1; i > 0; i--){
for (let j = 0; j < i; j++){
if (arr[j] > arr[j + 1]){
swap(arr,j, j + 1)
}
}
}
return arr
}
// 交换arr的i和j位置上的值
function swap(arr,i, j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
};
let array = [1,4,2,3,4,2,5,8,3];
console.log(bubbleSorting(array))
</script>
</body>
</html>
冒泡排序时间复杂度: 可以看出我们的代码中有两次for循环,在循环体中以一个常数操作
时间复杂度为:
O(
N
2
N^{2}
N2)
2.3 插入排序
2.3.1 插入排序概念
插入排序也是一种常见的排序算法,插入排序的思想是:将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。
插入排序的步骤如下:每次从无序部分中取出一个元素,与有序部分中的元素从后向前依次进行比较,并找到合适的位置,将该元素插到有序组当中。链接.
例:对于数组 [3,2,5,4,2,3,3] 进行插入排序的详细过程:
1、0~0位置上做到有序 ——>就一个数 做到了
2、0~1位置上做到有序 ——>2比3小 2 3互换位置——> [2,3,5,4,2,3,3]
3、0~2位置上做到有序 ——>5比3大 位置不动——> [2,3,5,4,2,3,3]
4、0~3位置上做到有序 ——>4比5小 4 5互换位置——> [2,3,4,5,2,3,3]——>4比3大 位置不动
5、0~4位置上做到有序 ——>2比5小 2 5互换位置——> [2,3,4,2,5,3,3]
——>2比4小 2 4互换位置——> [2,3,2,4,5,3,3]——>2比3小 2 3互换位置——> [2,2,3,4,5,3,3]
2比2相等 位置不动
6、0~5位置上做到有序 ——>3比5小 3 5互换位置——> [2,2,3,4,3,5,3]
——>3比4小 3 4互换位置——> [2,2,3,3,4,5,3]——>3比3相等 位置不动
7、0~6位置上做到有序 ——>3比5小 3 5互换位置——> [2,2,3,3,4,3,5]
——>3比4小 3 4互换位置——> [2,2,3,3,3,4,5]——>3比3相等 位置不动
2.3.2 插入排序时间复杂度分析
这里我们可以发现,插入排序的时间复杂度和其数据结构有一定的关系。而选择排序和冒泡排序其时间复杂度不会受数据结构的影响。
对于这种数组 [7,6,5,4,3,2,1] 其使用插入排序的时间复杂度是 O(
N
2
N^{2}
N2)级别的
对于这种数组[1,2,3,4,5,6,7] 其使用插入排序的时间复杂度是 O(N)级别的
但是对于一个算法的估计其时间复杂度时要按最差情况来估计,所以插入排序的时间复杂度为 O( N 2 N^{2} N2)级别的
2.3.2 插入排序代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>插入排序</title>
</head>
<body>
<script>
function insertSort(arr) {
if (arr == null || arr.length < 2){
return;
}
for (let i = 0; i < arr.length; i++){
for (let j = i; j > 0; j--){
if (arr[j] < arr[j-1]){
swap(arr,j, j-1);
}
}
}
return arr;
}
function swap(arr,i, j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
let array = [1,4,2,3,4,2,5,8];
console.log(insertSort(array))
</script>
</body>
</html>
2.4 二分法
二分法详解与扩展
2.4.1 某数是否存在
在一个有序数组中,找某个数是否存在 (要求时间复杂度小于O(N) )
在一个有序数组中[1,3,4,5,6,7,8,9]
每一次都将数组对半砍
假设我们要找到的数为 num = 3;则需要砍四次。
其时间复杂度就是 O(log2N) 简写为 O(logN)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>例一</title>
</head>
<body>
<script>
function findValueExists(arr, key) {
if (arr == null){
return;
}
let low = 0;
let high = arr.length -1;
while (low <= high){
let mid = low + ((high - low) >> 1);
if (key === arr[mid]){
return `该数组有 ${key} 在第 ${mid} 位`;
} else if (key > arr[mid]) {
low = mid + 1;
} else if (key < arr[mid]){
high = mid - 1;
}
}
return `该数字未有 ${key} 的值!`
}
let array = [1, 1, 2, 4, 5, 7]
console.log(findValueExists(array,5))
console.log(findValueExists(array,6))
</script>
</body>
</html>
2.4.1 某数最左侧位置
在一个有序数组中,找到>=某个数最左侧的位置
使用二分法 申请一个变量用于存储>=要找到的那个数的最左侧的位置。二分到其长度为1结束。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>例二</title>
</head>
<body>
<script>
// 在一个有序数组中,找到>=某个数最左侧的位置
function findValueExists(arr, key) {
if (arr == null){
return;
}
let low = 0;
let high = arr.length -1;
let leftPosition;
while (low <= high){
let mid = low + ((high - low) >> 1);
if (key === arr[mid]){
leftPosition = mid;
if (low === high){
return `${key} 在数组中的最左侧的位置位为第 ${low} 位`;
} else {
high = mid - 1;
}
} else if (key > arr[mid]) {
low = mid + 1;
} else if (key < arr[mid]){
high = mid - 1;
}
}
return `该数字未有 ${key} 的值!`
}
let array = [1, 1, 2, 4, 5, 5, 5, 7]
console.log(findValueExists(array,5))
console.log(findValueExists(array,6))
</script>
</body>
</html>
2.4.1 局部最小值问题
局部最小值问题
在一个无序数组中,任何两个相邻的数一定不相等,
局部最小:对于数组的0位置上的数小于1位置上的数,那么0位置上的数就是局部最小位置
对于数组的n-1位i置上的数小于n-2位置上的数,那么n-1位置上的数就是局部最小位置
对于数组的i位置上的数小于i-1和i+1位置上的数,那么i位置上的数就是局部最小位置
求一个局部最小的位置。要求其时间复杂度小于O(N)。
解:
首先我们判断0位置和n-1位置上的数是不是局部最小,如果是直接返回 如果不是则中间必存在局部最小位置。因为数组的左边是向下的趋势,数组右边是像上的趋势。故中间必存在拐点。
然后我们二分取中间的值进行判断。如果该值也不满足局部最小 ,则同理继续二分,总会找到一个局部最小的位置。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>例三</title>
</head>
<body>
<script>
// 局部最小值问题
// 在一个无序数组中,任何两个相邻的数一定不相等,
// 局部最小:对于数组的0位置上的数小于1位置上的数,那么0位置上的数就是局部最小位置
// 对于数组的n-1位i置上的数小于n-2位置上的数,那么n-1位置上的数就是局部最小位置
// 对于数组的i位置上的数小于i-1和i+1位置上的数,那么i位置上的数就是局部最小位置
// 求一个局部最小的位置。要求其时间复杂度小于O(N)。
function localMinimum(arr) {
if (arr == null){
return;
}
let low = 0;
let high = arr.length -1;
if(arr[0] < arr[1]) return `在数组的第0位存在局部最小位置`;
if(arr[arr.length - 1] < arr[arr.length -2])
return `在数组的第 ${arr.length -1} 位存在局部最小位置`;
while (low <= high){
let mid = low + ((high - low) >> 1);
if ((arr[mid -1] > arr[mid]) && (arr[mid + 1] > arr[mid])){
return `在数组的第 ${mid} 位存在局部最小位置`;
} else if (arr[mid-1] < arr[mid] || arr[mid] < arr[mid + 1]){
high = mid - 1;
} else if (arr[mid-1] > arr[mid] || arr[mid] > arr[mid + 1]){
low = mid + 1;
}
}
}
let array = [6, 5, 4, 3, 2, 7, 9]
console.log(localMinimum(array))
</script>
</body>
</html>
2.5 递归
剖析递归行为和递归行为时间复杂度的估算
递归的两个特点:
1、调用自身
2、结束条件
2.5.1 master公式
master公式的使用
T(N) = a * T ( N / b ) + O (
N
d
N^{d}
Nd )
(注:)
T(N) 代表:母问题的数据量是N级别的
T ( N / b ) 代表:子问题的规模都是 N / b 规模 (所有子问题是等量的)
a 代表:子问题被调用了多少次
O (
N
d
N^{d}
Nd ) 代表:除了子问题被调用之外,其他式子的时间复杂度是多少
1)log(b , a) > d ——> 复杂度为 O(
N
l
o
g
(
b
,
a
)
N^{log(b , a)}
Nlog(b,a))
2)log(b , a) = d ——> 复杂度为 O(
N
d
N^{d}
Nd * logN)
3)log(b , a) < d ——> 复杂度为 O(
N
d
N^{d}
Nd)
2.5.2 递归找最大值
例子:用递归的方法找一个数组的最大值,系统上是怎么做到的?
代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>求最大值</title>
</head>
<body>
<script>
function process(arr, l, r) {
if (l === r){
return arr[l];
}
let mid = l + ((r - l) >> 1);
let leftMax = process(arr, l , mid)
let rightMax = process(arr, mid + 1 , r)
return Math.max(leftMax, rightMax)
}
function getMax(arr) {
return process(arr, 0, arr.length - 1);
}
let array = [1,2,1,3,13,12,1,7,3,9]
console.log(getMax(array))
</script>
</body>
</html>
过程示例:
使用master公式分析该问题的时间复杂度:
可以看出该问题的母问题的规模是 N ,递归了两次(a = 2),
每次调用自身时子问题的规模都是 N/2 (b = 2), 其他式子的时间复杂度为 O(1) (d = 0),该算法符合master公式
用master公式可以表达为: T(N) = 2 * T ( N / 2 ) + O ( 1 )
又因为:log(b,a) = log(2, 2) = 1 > d ——> 所以这个算法的时间复杂度为 O(
N
l
o
g
(
b
,
a
)
N^{log(b , a)}
Nlog(b,a)) = O(N)
2.6 归并排序
1)整体就是简单的递归,左边排好序、右边排好序、让整体有序
2)让其整体有序的过程里用了排外序的方法
3)利用master公式来求解时间复杂度
4)归并排序的实质
时间复杂度O(N*logN),额外空间复杂度O(N)
2.6.1 归并排序流程
对于一个数组从中点的位置分开,先让左侧部分排好序,再让右边部分排好序,然后整体整合。
将图中左侧部分和右侧部分分别排好序,然后使用两个指针分别从两部分的最左侧开始,在内存中单独开辟一个空间 ,这时我们比较两个指针指向的数的大小,左侧小于等于右侧的时候,将左侧部分指针指向的值拷贝到辅助空间中,然后左侧指针右移一位。如果右侧部分指针指向的值小于左侧的,则将右侧部分指针指向的值拷贝到辅助空间中,然后右侧指针右移一位。依次循环,如果哪侧越界了,将剩下的部分直接拷贝到辅助空间中。将辅助空间拷贝到原数组。
2.6.2 归并排序代码示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>归并排序</title>
</head>
<body>
<script>
function mergeSort(arr) {
process(arr, 0, arr.length-1);
}
function process(arr, l, r) {
if (l === r){
return arr[l];
}
let mid = l + ((r - l) >> 1);
process(arr, l, mid);
process(arr, mid + 1, r);
merge(arr, l, mid, r);
}
function merge(arr, l, mid, r) {
let help = [];
let leftIndex = l;
let rightIndex = mid + 1;
let i = 0;
while (leftIndex <= mid && rightIndex <= r){
help[i++] = arr[leftIndex] <= arr[rightIndex]
? arr[leftIndex++] : arr[rightIndex++]
}
while (leftIndex <= mid){
help[i++] = arr[leftIndex++]
}
while (rightIndex <= r){
help[i++] = arr[rightIndex++]
}
for (let i = 0; i < help.length; i++){
arr[l + i] = help[i];
}
}
let array = [1,3,14,1,3,7,4,6,9];
mergeSort(array);
console.log(array)
</script>
</body>
</html>
2.6.3 归并排序时间复杂度分析
这里分析该代码的时间复杂度要用到master公式,
可以看出该问题的母问题的规模是 N ,递归了两次(a = 2)
每次调用自身时子问题的规模都是 N/2 (b = 2), 其他式子的时间复杂度为 O(N) (d = 1)
该算法符合master公式
用master公式可以表达为: T(N) = 2 * T ( N / 2 ) + O ( N )
又因为:log(b,a) = log(2, 2) = 1 = d ——>
所以这个算法的时间复杂度为 O(
N
d
N^{d}
Nd * logN) = O( N * logN)
2.6.4 归并排序扩展
2.6.4.1 小和问题
问:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和,求数组的小和。
例子:[1,3,4,2,5] 1左边比1小的数没有;3左边比3小的数:1。4左边比4小的数:1,3。2左边比2小的数:1。5左边比5小的数:1、3、4、2;所以小和为 1+1+3+1+1+3+4+2=16。
要求其时间复杂度小于 O(
N
2
N^{2}
N2)
思路:逆向思维:对于1来说右边有几个数比它大,就会产生几个1的小和。同理对于剩下的元素,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>小和问题</title>
</head>
<body>
<script>
function mergeSort(arr) {
return process(arr, 0, arr.length-1)
}
// 既要求小和又要排序
function process(arr, l, r) {
if (l === r){
return 0;
}
let mid = l + ((r - l) >> 1);
return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r);
}
function merge(arr, l, mid, r) {
let help = [];
let leftIndex = l;
let rightIndex = mid + 1;
let i = 0;
let res = 0;
while (leftIndex <= mid && rightIndex <= r){
res += arr[leftIndex] < arr[rightIndex] ?
(r - rightIndex + 1) * arr[leftIndex] : 0;
help[i++] = arr[leftIndex] < arr[rightIndex] ?
arr[leftIndex++] : arr[rightIndex++]
}
while (leftIndex <= mid){
help[i++] = arr[leftIndex++]
}
while (rightIndex <= r){
help[i++] = arr[rightIndex++]
}
for (let i = 0; i < help.length; i++){
arr[l + i] = help[i];
}
return res;
}
let array = [1, 3, 4, 2, 5];
console.log(mergeSort(array))
console.log(array)
</script>
</body>
</html>
2.6.4.2 逆序对问题
问:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印所有逆序对。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>逆序对问题</title>
</head>
<body>
<script>
// 问:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请打印多有逆序对。
function mergeSort(arr) {
process(arr, 0, arr.length-1);
}
function process(arr, l, r) {
if (l === r){
return arr[l];
}
let mid = l + ((r - l) >> 1);
process(arr, l, mid);
process(arr, mid + 1, r);
merge(arr, l, mid, r);
}
function merge(arr, l, mid, r) {
let help = [];
let leftIndex = l;
let rightIndex = mid + 1;
let i = 0;
while (leftIndex <= mid && rightIndex <= r){
if(arr[leftIndex] > arr[rightIndex])
console.log(arr[leftIndex], arr[rightIndex]);
help[i++] = arr[leftIndex] > arr[rightIndex] ?
arr[leftIndex++] : arr[rightIndex++];
}
while (leftIndex <= mid){
help[i++] = arr[leftIndex++];
}
while (rightIndex <= r){
help[i++] = arr[rightIndex++];
}
for (let i = 0; i < help.length; i++){
arr[l + i] = help[i];
}
}
let array = [1, 3, 4, 2, 5];
mergeSort(array);
console.log(array);
</script>
</body>
</html>
2.7 荷兰国旗问题
2.7.1 问题一
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外的空间复杂度为O(1),时间复杂度为O(N)
思路:申请一个j变量用于表示<=区域边界,再申请一个指针 i 表示正在判断的数的位置,从数组第一个数开始遍历,
1)当 i 所指的数小于等于num时,将i所指的数与<=区域下一个数交换,<=区域阔 1(j++),i++
2)当 i 所指的数大于num时, 直接 i++
代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>荷兰国旗问题一</title>
</head>
<body>
<script>
// 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
// 要求额外的空间复杂度为O(1),时间复杂度为O(N)
function dutchFlagIssue(arr, num) {
let j = -1;
for (let i = 0; i < arr.length; i++){
if(arr[i] <= num){
swap(arr, i, j+1);
j++;
}
}
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1, 3, 4, 2, 5];
dutchFlagIssue(array, 3);
console.log(array);
</script>
</body>
</html>
2.7.2 荷兰国旗问题
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num 的数放在数组的中间,大于num的数放在数组的右边。要求额外的空间复杂度为O(1),时间复杂度为O(N)。
代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>荷兰国旗问题</title>
</head>
<body>
<script>
// 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,
// 等于num 的数放在数组的中间,大于num的数放在数组的右边
// 要求额外的空间复杂度为O(1),时间复杂度为O(N)
function dutchFlagIssue(arr, num) {
let l = -1;
let r = arr.length;
let i = 0;
while (i < r){
if(arr[i] < num){
swap(arr, i, l + 1);
l++;
i++;
} else if(arr[i] > num){
swap(arr, i, r - 1);
r--;
} else {
i++;
}
}
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1, 3, 4, 2, 6, 9, 5];
dutchFlagIssue(array, 5);
console.log(array);
</script>
</body>
</html>
2.8 快速排序
2.8.1 快速排序1.0
根据荷兰国旗问题,将一个数组的最后一个数作为荷兰国旗问题的num,然后按这个数将整个数组剩下的分为小于等于该数和大于该数的左右两组,再将最后一个数与大于该数的那组数的第一个数互换位置。这个时候就可以判定该数已经被排序好了,然后左右两组数重复上面的操作以此递归。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>快速排序1.0</title>
</head>
<body>
<script>
// 原则数组的最后一个数作为分界点 将其他数分为小于等于此数和大于次数两类
// 最后将最后这个数与 分界点后一个数调换,这样这个数的位置就被确定下来了
function process(arr) {
if (arr == null || arr.length < 2){
return;
}
quickSort(arr, 0, arr.length - 1);
}
function quickSort(arr, l, r) {
if (l < r){
let mid = partition(arr, l , r);
quickSort(arr, l, mid - 1);
quickSort(arr, mid + 1, r);
}
}
function partition(arr, l , r) {
let j = l - 1;
for (let i = l; i < r; i++){
if(arr[i] <= arr[r]){
swap(arr, i, ++j);
}
}
swap(arr, r, j+1);
return j + 1;
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1, 3, 4, 2, 5, 3, 9, 2,34,22,2,4,2,9,7,5];
process(array);
console.log(array);
</script>
</body>
</html>
2.8.2 快速排序2.0
1)、把数组范围中的最后一个是数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧 < 划分值 、 中间==划分值、 右侧 > 划分值
2)、对左侧范围和右侧范围,递归执行
分析:
1):划分值越靠近两侧,复杂度越高; 划分值越靠近中间,复杂度越低
2):可以轻而易举的举出最差的例子,所以不改进快速排序的时间复杂度为O(
N
2
N^{2}
N2 )
3):额外空间复杂度最差情况为O(N),好情况下为O(logN)级别。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>快速排序2.0</title>
</head>
<body>
<script>
// 比快排1.0版本稍微快一点 因为在partition时搞定的是一批等于最后一个值的数,
// 这一批等于最后一个数的位置都被确定并返回
// 1.0在partition只搞定了一个。
function process(arr) {
if (arr == null || arr.length < 2){
return;
}
quickSort(arr, 0, arr.length - 1);
}
function quickSort(arr, l, r) {
if (l < r){
let p = partition(arr, l , r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
function partition(arr, l, r) {
let less = l -1;
let more = r;
while (l < more){
if (arr[l] < arr[r]){
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]){
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return [less + 1, more];
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1, 3, 4, 2, 6, 9, 5];
process(array);
console.log(array);
</script>
</body>
</html>
2.8.3 快速排序3.0
1)、从数组中随机选取一个数作为划分值,然后把数组通过荷兰国旗问题分成三个部分:
左侧 < 划分值 、 中间==划分值、 右侧 > 划分值
2)、对左侧范围和右侧范围,递归执行
分析:
就因为这个随机选取划分值的行为将整体算法的时间复杂度变为O(N*logN)。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>快速排序3.0</title>
</head>
<body>
<script>
function process(arr) {
if (arr == null || arr.length < 2){
return;
}
quickSort(arr, 0, arr.length - 1);
}
function quickSort(arr, l, r) {
if (l < r){
// 就比2.0多了一个随机选取分界数的过程。
// 就因为有这个随机行为 时间复杂度就变成了 O(N*logN)
// 其复杂度的证明和概率有关
swap(arr, Math.floor(Math.random() * (r - l + 1) + l), r);
let p = partition(arr, l , r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1, r);
}
}
function partition(arr, l, r) {
let less = l -1;
let more = r;
while (l < more){
if (arr[l] < arr[r]){
swap(arr, ++less, l++);
} else if (arr[l] > arr[r]){
swap(arr, --more, l);
} else {
l++;
}
}
swap(arr, more, r);
return [less + 1, more];
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
let array = [1, 3, 4, 2, 6, 9, 5];
process(array);
console.log(array);
</script>
</body>
</html>
2.9 堆
2.9.1 堆结构
1、堆结构就是用数组实现的完全二叉树结构
2、完全二叉树中如果每棵子树的最大值都在顶部的就是大根堆
3、完全二叉树中如果每棵子树的最小值都在顶部的就是小根堆
4、堆结构的heapInsert与heapify操作
5、堆结构的增大和减小
6、优先级队列结构,就是堆结构
堆结构的两个基本操作:heapInsert与heapify
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>堆结构的基本操作</title>
</head>
<body>
<script>
// 某个数现在处在index位置,往上继续移动
function heapInsert(arr, index) {
// 当他比他的父节点大的时候,就将这个数与父节点数交换位置
// 以此往上推,直到它没有它父级的数大为止
while (arr[index] > arr[(index - 1) >>2]) {
swap(arr, index, (index - 1) >>2);
index = (index - 1) >>2
}
}
// 某个数在index位置,能否往下移动
function heapify(arr, index, heapSize) {
let left = index * 2 + 1; // 左孩子的下标
while (left < heapSize) { // 下方还有孩子的时候
// 比较两个孩子的值,谁的值大把谁的下标给largest
let largest = left + 1 < heapSize && arr [left + 1] > arr[left] ? left + 1 : left;
// 选出来的较大的那个孩子和它本身比较,谁大谁把下标给largest
largest = arr[index] > arr[largest] ? index : largest;
// 如果最终选出来的最大的值是他自身 则终止下移
if (largest === index) break;
// 如果最终选出来的最大的值不是他自身 则它与较大的那个值交换
swap(arr, index, largest);
index = largest;
left = index * 2 + 1;
}
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
};
</script>
</body>
</html>
2.9.2 堆排序
利用堆的结构和性质对数组进行排序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>堆排序</title>
</head>
<body>
<script>
// 整体的时间复杂度就是 O(N*logN)
// 额外的空间复杂度就是 O(1) 只有堆排序可以实现
function heapSort(arr) {
if (arr === null || arr.length < 2) {
return;
}
// // 首先来个循环使用heapInsert将数组整理成大根堆的形式
// for (let i = 0; i < arr.length; i++){ // O(N)
// heapInsert(arr, i); // O(logN)
// }
// 整理成大根堆的形式还有更快一点点的方法(比用heapInsert快一点点但是整体的时间复杂度还是O(N*logN)) 如下
for (let i = arr.length; i >= 0; i--){
heapify(arr, i, arr.length);
}
let heapSize = arr.length;
// 因为已经成为了大根堆的形式了 ,所以这个时候
// 该数组的第一个数一定是该数组中最大的数
// 将最大的数放到最后的位置上
swap(arr, 0, --heapSize);
while (heapSize > 0){ // O(N)
// 上面将最大值放在最后的位置上后剩下的0-heapSize的位置已经不是大根堆
// 使用heapify函数将第0位置上的数整理成大根堆的形式
heapify(arr, 0, heapSize); // O(logN)
// 此时数组 0-heapSize位置又变成了大根堆
// 再次将0位置上的最大的数与大根堆最后的数交换 大根堆长度减一
swap(arr, 0, --heapSize); // O(1)
// 以此操作循环执行 直至数组上的数都整理遍
}
}
// 某个数现在处在index位置,往上继续移动
function heapInsert(arr, index) {
// 当他比他的父节点大的时候,就将这个数与父节点数交换位置
// 以此往上推,直到它没有它父级的数大为止
while (arr[index] > arr[(index - 1) >>1]) {
swap(arr, index, (index - 1) >>1);
index = (index - 1) >>1
}
}
// 某个数在index位置,能否往下移动
function heapify(arr, index, heapSize) { // heapSize要比现阶段已经排好成大根堆结构的长度大一
let left = index * 2 + 1; // 左孩子的下标
while (left < heapSize) { // 下方还有孩子的时候
// 比较两个孩子的值,谁的值大把谁的下标给largest
let largest = left + 1 < heapSize && arr [left + 1] > arr[left] ? left + 1 : left;
// 选出来的较大的那个孩子和它本身比较,谁大谁把下标给largest
largest = arr[index] > arr[largest] ? index : largest;
// 如果最终选出来的最大的值是他自身 则终止下移
if (largest === index) break;
// 如果最终选出来的最大的值不是他自身 则它与较大的那个值交换
swap(arr, index, largest);
index = largest;
left = index * 2 + 1;
}
}
function swap(arr,i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
let array = [1, 3, 4, 2, 6, 9, 5];
heapSort(array)
console.log(array);
</script>
</body>
</html>
2.9.3 手写一个堆结构
手写一个堆结构,实现两个功能:
1):一个是往堆结构中添加数(添加完之后自行调整推结构,始终保持大根堆的结构)。
2):一个是从堆结构中抛出现有堆结构的最大数,剩下的数自行调整为大根堆的结构。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>手写堆结构</title>
</head>
<body>
<script>
function HeapStructurePackaging() {
this.arr = []
this.add = function (x) {
this.arr.push(x)
heapInsert(this.arr, this.arr.length - 1)
}
this.poll = function () {
let returnValue = this.arr[0];
swap(this.arr, 0, this.arr.length - 1);
heapify(this.arr, 0, this.arr.length - 1);
this.arr.splice(this.arr.length -1, 1);
return returnValue;
}
function swap (arr, i, j) {
let tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
function heapInsert(arr, index) {
while (arr[index] > arr[(index - 1) >>1]) {
swap(arr, index, (index - 1) >>1);
index = (index - 1) >>1
}
}
function heapify(arr, index, heapSize) {
let left = index * 2 + 1;
while (left < heapSize) {
let largest = left + 1 < heapSize && arr [left + 1] > arr[left] ? left + 1 : left;
largest = arr[index] > arr[largest] ? index : largest;
if (largest === index) break;
swap(arr, index, largest);
index = largest;
left = index * 2 + 1;
}
}
}
// 下面是对封装好的大根堆的应用
let bigRootPile = new HeapStructurePackaging();
let array1 = [9,33,26,2,11,8,22,13,41,4,10,57];
for (let i = 0; i < array1.length; i++){
bigRootPile.add(array1[i]);
}
console.log(bigRootPile);
let i = array1.length - 1;
while (bigRootPile.arr.length !== 0){
array1[i--] = bigRootPile.poll();
}
console.log(bigRootPile);
console.log(array1)
</script>
</body>
</html>
2.10 计数排序
计数排序是不基于比较的排序
不基于比较的排序,都是根据具体的数据状况做的排序
应用范围不广,很窄的一路数据排序算法
假设一个整数数组,里面的数都是员工的年龄
分析: 由于员工的年龄是1860岁之间,所以我们假设数据的范围为0100
我们申请一个长度为100的数组,我们规定在数组的第18位置上的数为18岁的人有多少个(频数)。
即下标为年龄,位置上的数为该年龄的数量。
该算法的时间复杂度为O(N)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>计数排序</title>
</head>
<body>
<script>
function countSort(arr) {
if(arr === null || arr.length < 2) {
return;
}
let minValue = Math.min(...arr);
let array = [];
for (let i = 0; i < arr.length; i++){
if (array[arr[i] - minValue]){
array[arr[i] - minValue] ++;
} else {
array[arr[i] - minValue] = 1;
}
}
let arrayTwo = []
for (let i = 0; i < array.length; i++){
if(array[i]){
for (let j = 0; j < array[i]; j++){
arrayTwo.push(i + minValue)
}
}
}
return arrayTwo;
}
let array = [4,7,5,5,6,9,7,4,4,3,12];
console.log(countSort(array))
</script>
</body>
</html>
2.11 基数排序
也是不基于比较的排序。基数排序要求所排的数必须得有进制。
示例:假设有个数组[17, 13, 25, 100, 72] 该数组的所有数都为十进制的数,找到最大位数的数,这里是100为三位,所以 将其他低于该位的数前面自动补0。原数组变为——> [017, 013, 025, 100, 072]
准备十个队列结构的桶。
1、根据个位上的数,从左到右将数组中的数字放入对应的桶中:
把桶中的数据从左到右依次倒出来:[100, 072, 013, 025, 017]
2、根据十位上的数,将第一次的结果:[100, 072, 013, 025, 017]从左到右将数组中的数字放入对应的桶中:
再次把桶中的数据从左到右依次倒出来:[100, 013, 017, 025,072]
3、根据十位上的数,将第一次的结果:[100, 013, 017, 025,072]从左到右将数组中的数字放入对应的桶中:
再次把桶中的数据从左到右依次倒出来:[013,017, 025,072,100]
大家注意到了嘛 现在数已经排好序了。
代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>基数排序</title>
</head>
<body>
<script>
function cardinalitySort(arr) {
if (arr === null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxBits(arr))
}
function maxBits(arr) {
// 求一个数组的最大值的位数
let max = 0;
max = Math.max(...arr);
let res = 0;
while (max !== 0){
res ++;
max = 0|(max/10); // 去除小数部分
}
return res;
}
function radixSort(arr, l, r, digit) { // digit,表示arr数组中最大的数有几个十进制位
const radix = 10;
// let i = 0, j = 0;
let bucket = new Array(radix).fill(0);
for (let d = 1; d <= digit; d++){ // 这个for循环代表有多少位就进出几次
let count = new Array(radix).fill(0); // 定义一个10位的数组并用0自动填充
for (let i = l,j = 0; i <= r; i++){
// 统计arr数组第l到r上各数d位上的数的频数
j = getDigit(arr[i], d);
// count下标代表该数,count该下标上的值代表(该下标值)对应的频数
count[j] ++;
}
for (let i = 1; i < radix; i++){
// 将已经统计好的频数的count 改造成每位数都是前面所有数累加的形式
count[i] = count[i] + count[i - 1]
}
for (let i = r; i >= l; i--){
// 从右到左遍历arr数组上的每个数
let j;
j = getDigit(arr[i], d); // 获取 arr数组上第i位的数 求该数的第d位的值付给j
bucket[count[j] - 1] = arr[i];
// 找到该数第d位值得频数累加值
// 将数组第i位置上得数放入辅助数组bucket的累计频数频数减一的位置
count[j]--; // 将该数第d位值对应的频数累加值减一
}
for (let i = l, j = 0; i <= r; i++,j++){
arr[i] = bucket[j]
// 将排好序的辅助数组返给 原数组。
}
}
}
function getDigit (x, d){
// 用于返回x的数上d位上的数
// %代表模运算也就是取余数
// 0|number 代表直接丢弃number的小数部分取整
return 0|((x % Math.pow(10, d)) / Math.pow(10, d -1))
}
let array = [1, 3, 4, 34, 23,64,23,91,2, 6, 9, 1115];
console.log(maxBits(array));
cardinalitySort(array);
console.log(array)
// 创建一个长度为9的数组并用0填充各位置
console.log(new Array(9).fill(0));
</script>
</body>
</html>
2.12 排序总结
2.12 排序总结
2.12.1 排序算法的稳定性及其汇总
值相同的数之间,如果不因为排序而改变相对次序,就是这个排序是有稳定性的;否则就没有。
不具备稳定性的排序:
选择排序、快速排序、堆排序
具备稳定性的排序:
冒泡排序、插入排序、归并排序、一切桶排序思想下的排序
目前没有找到时间复杂度0(N * logN),额外空间复杂度为O(1),又稳定的排序。
排序方法 | 时间复杂度 | 空间复杂度 | 是否稳定性 |
---|---|---|---|
选择排序 | O( N 2 N^{2} N2) | O(1) | × |
冒泡排序 | O( N 2 N^{2} N2) | O(1) | √ |
插入排序 | O( N 2 N^{2} N2) | O(1) | √ |
归并排序 | O(N*logN) | O(N) | √ |
快速排序 | O(N*logN) | O(logN) | × |
堆排序 | O(N*logN) | O(1) | × |
一般排序会选择快速排序,因为其时间复杂度为O(N*logN),快速排序它的常数项经过实验的结果它是最低的。时间最短。
空间复杂度要求较低时用堆排序,要求有稳定性用归并排序。
2.12.2 结论
1、基于比较的排序能不能做到时间复杂度在 O(N * logN) 以下——>不能(目前没有找到)
2、基于比较的排序能不能在时间复杂度为 O(N * logN),空间复杂度在O(N)以下,还能做到稳定——>不能
2.12.3 常见的坑
1、归并排序的额外空间复杂度可以变成 O(1) ,但是非常困难,不需要掌握,有兴趣可以搜“归并排序 内部缓存法”。但是变成 O(1) 后其就不再稳定了。
2、“原地归并排序”会让额外空间复杂度为 O(1) ,但是会让归并排序的时间复杂度变成 O(
N
2
N^{2}
N2)
3、快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜论文:“ 01 stable sort ” 但是做到的同时会让空间复杂度变成 O(N)
4、所有的改进都不重要,因为目前没有找到时间复杂度 O(N * logN) ,额外空间复杂度 O(1) ,又稳定的排序
5、有一道题目,时奇数放在数组的左边,偶数放在数组的右边,还要求原始的相对次序不变,碰到这个问题,可以怼面试官。