大O表示法
一个算法的效率可以用程序的时间复杂度来分析,时间复杂度就是分析一个算法对于给定数量的输入需要多长时间来完成任务。这通常定义为大O表示法,就是将算法的所有步骤转换为代数项,然后排除不会对问题的整体复杂度产生较大影响的较低阶常数和系数,简单理解也就是时间复杂度主要看自变量n的变化,算法的速度会跟随着数据量变化。
大O复杂度的排序,从上至下时间复杂度依次增大:
O(1)—常量时间:给定一个大小为n的输入,概算法只需要一步就可以完成任务。
O(log n)— 对数时间:给定大小为n的输入,该算法每执行一步,它完成任务所需要的步骤数目会以一定的因子减少。
O(n)— 线性时间:给定大小为n的输入,该算法完成任务所需要的步骤直接和n相关(1对1的关系)。
O(n²)—二次方时间:给定大小为n的输入,完成任务所需要的步骤是n的平方。
O(C^n)— 指数时间:给定大小为n的输入,完成任务所需要的步骤是一个常数的n次方(非常大的数字)。
常见的大O表示形式的图示
常见排序算法的性能
排序算法稳定性的简单形式化定义为:如果arr[i] = arr[j],排序前arr[i]在arr[j]之前,排序后arr[i]还在arr[j]之前,则称这种排序算法是稳定的。通俗地讲就是保证排序前后两个相等的数的相对顺序不变。
交换数组中某两个位置的值的方法
function swap(m,n,arr){
let temp=arr[m];
arr[m]=arr[n];
arr[n]=temp;
}
排序算法
冒泡排序
冒泡排序就是把小的元素往前调或者把大的元素往后调,每次比较是相邻的两个元素进行比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换也会把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法,但是相对其他排序运行效率较低。
冒泡排序算法原理:每次只对相邻两个元素进行操作,每次都会比较相邻两个元素的大小,若不满足排序要求,就将它俩交换。每一次冒泡,会将一个元素移动到它相应的位置,该元素就是未排序元素中最大的元素。
冒泡排序的思路:
- 比较相邻的元素。
- 如果左边的元素大, 则两个元素交换位置
- 再向右移动一个位置, 比较下面两个元素
- 当走到最右端时, 最大的元素一定被放在了最右边
- 按照这个思路, 从最左端重新开始, 这次右边的第二个位置就是第二大元素.
- 依次类推, 就可以将数据排序完成
就可以循环嵌套循环实现
冒泡排序的时间复杂度:
最好情况:只需要进行一次冒泡操作此时所有元素按顺序排好,没有任何元素发生交换,此时就可以结束程序,所以最好情况时间复杂度是O(n).
最坏情况: 要排序的数据完全倒序排列的,我们需要进行n次冒泡操作,每次冒泡时间复杂度为O(n),所以最坏情况时间复杂度为O(n^2)。
平均复杂度:O(n^2)
选择排序
选择排序的思路:
- 选定第一个索引位置,然后和后面元素依次比较
- 如果后面的队员, 小于第一个索引位置的队员, 则交换位置
- 经过一轮的比较后, 可以确定第一个位置是最小的
- 然后使用同样的方法把剩下的元素逐个比较即可
- 可以看出选择排序,第一轮会选出最小值,第二轮会选出第二小的值,直到最后
选择排序的效率:最好情况和最坏情况都需要遍历未排序区间,找到最小元素。所以都为O(n^2).因此,平均复杂度也为O(n^2).因为元素都要进行交换,这样如果遇到相同的元素,也会使他们的顺序发生交换。不是稳定算法。
插入排序
插入排序原理:首先我们将数组中的数据分为两个区间,一个是已排序区间,另一个是未排序区间,同时这两个区间都是动态的。开始时,假设最左侧的元素已被排序,即为已排序区间,每一次将未排序区间的首个数据放入排序好的区间中,直达未排序空间为空。
插入排序的思路:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出未排序的第一个元素,在已经排序的元素序列中从后向前一直进行比较
- 如果该元素(已排序中)大于新元素,将该元素移到下一位置
- 重复上一个步骤,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后, 重复上面的步骤.
插入排序的时间复杂度
最好情况: 即该数据已经有序,我们不需要移动任何元素。于是我们需要从头到尾遍历整个数组中的元素O(n) .
最坏情况: 即数组中的元素刚好是倒序的,每次插入时都需要和已排序区间中所有元素进行比较,并移动元素。因此最坏情况下的时间复杂度是O(n^2) .
平均时间复杂度:类似我们在一个数组中插入一个元素那样,该算法的平均时间复杂度为O(n^2) .
在插入的过程中,如果遇到相同的元素,可以选择将其插入到之前元素的前面也可以选择插入到后面。所以,插入排序可以是稳定的也可能是不稳定的。
快速排序
快速排序是什么?
- 希尔排序相当于插入排序的升级版, 快速排序其实是最慢的冒泡排序的升级版.
- 冒泡排序需要经过很多次交换, 才能在一次循环中, 将最大值放在正确的位置.
- 而快速排序可以在一次循环中(其实是递归调用)找出某个元素的正确位置, 并且该元素之后不需要任何移动.
快速排序,也就是“快排”。其实,快排是利用的分治思想。它具体的做法是在数组中取一个基准pivot,pivot位置可以随机选择(可以选择数组中的中间一个元素)。选择完pivot之后,将小于或等于pivot的所有元素放在pivot左边,将大于pivot的所有元素放在右边。最终,pivot左侧元素都将小于右侧元素。接下来需要递归将左侧的子数组和右侧子数组进行快速排序。如果左右两侧的数组都是有序的话,那么整个数组就处于有序的状态了。
快速排序的主要步骤为: 1. 挑选基准值:从数组中挑出一个元素,称为“基准”(pivot) 2. 分割:重新排序数组,所有比pivot小或者相等的元素摆放在pivot前面,所有比pivot值大的元素放在pivot后面。 3. 递归排序子数组:递归地将小于pivot元素的子序列和大于pivot元素的子序列进行快速排序。 4. 递归的判断条件是数列的大小是零或一。
如果每次分区都刚好把数组分成两个大小一样的区间,那么它的时间复杂度也为O(nlogn).但是如果遇到最坏情况下,该算法可能退化成O(n^2).
比如我们下面有这样一顿数字需要排序:
- 第一步: 从其中选出了65. (其实可以是选出任意的数字, 我们以65举个栗子)
- 第二步: 我们通过算法: 将所有小于65的数字放在65的左边, 将所有大于65的数字放在65的右边.
- 第三步: 递归的处理左边的数据.(比如你选择31来处理左侧), 递归的处理右边的数据.(比如选择75来处理右侧, 当然选择81可能更合适)
- 最终: 排序完成
归并排序
归并排序的基本思路就是先把数组一分为二,然后分别把左右数组排好序,再将排好序的左右两个数组合并成一个新的数组,最后整个数组就是有序的了。
基本思想与过程:先递归的分解数列,再合并数列(分治思想的典型应用)
(1)将一个数组拆成A、B两个小组,两个小组继续拆,直到每个小组只有一个元素为止。
(2)按照拆分过程逐步合并小组,由于各小组初始只有一个元素,可以看做小组内部是有序的,合并小组可以被看做是合并两个有序数组的过程。
(3)对左右两个小数列重复第二步,直至各区间只有1个数。 下面对数组【42,20,17,13,28,14,23,15】进行归并排序,模拟排序过程如下:
第一步:拆分数组,一共需要拆分三次(logN);
第一次拆成【42,20,17,13】,【28,14,23,15】,
第二次拆成【42,20】,【17,13】,【28,14】,【23,15】,、
第三次拆成【42】,【20】,【17】,【13】,【28】,【14】,【23】,【15】;
第二步:逐步归并数组,采用合并两个有序数组的方法,每一步其算法复杂度基本接近于O(N)
第一次归并为【20,42】,【13,17】,【14,28】,【15,23】
第二次归并为【13,17,20,42】,【14,15,23,28】,
第三次归并为【13, 14, 15, 17, 20, 23, 28, 42】
归并排序的时间复杂度为O(nlongn)
搜索算法
顺序查找
顺序查找:就是从第一个元素开始,按索引顺序遍历待查找序列,直到找出给定目标或者查找失败,就用for循环。
时间复杂度:O(n)
应用:适合于存储结构为顺序存储或链接存储的线性表
二分法查找
- 二分查找:首先要找到一个中间值,通过与中间值比较,大的放右边,小的放在左边。再在两边中寻找中间值,持续以上操作,直到找到所在位置为止
- 时间复杂度:O(log₂n)
- 应用:适用于不经常变动而查找频繁的有序列表
将上诉算法封装为一个类
class Sort {
constructor() {
this.item = [];
}
push(ele) {
this.item.push(ele)
}
toString() {
return this.item.join("-")
}
swap(m, n) {
// 交换两个位置的值
let temp = this.item[n];
this.item[n] = this.item[m]
this.item[m] = temp
}
bubbleSort() {
// 控制趟数
for (let i = 0; i < this.item.length - 1; i++) {
// 控制每一趟比较元素的次数
for (let j = 0; j < this.item.length - 1 - i; j++) {
// 比较,前面一个元素大就交换元素
if (this.item[j + 1] < this.item[j]) {
this.swap(j, j + 1)
}
}
}
}
// 选择排序
selectionSort() {
// 控制趟数
for (let i = 0; i < this.item.length - 1; i++) {
// 第一趟确定的最小值的索引0,第一个元素
let min = i;
// 控制每一趟比较元素的次数
for (let j = i + 1; j < this.item.length; j++) {
// 第一趟的最小值的下一个元素索引是1,第二个元素
if (this.item[j] < this.item[min]) {
// 第一趟第二个元素比第一个元素小,就将第二个元素改为最小值然后再交换位置,下一次就是新的最小值作为第二个元素和第三个元素进行比较
min = j
}
}
// 第一趟,如果第一个元素比第二个元素则第一个元素还是最小值,直接交换位置后下一次还是之前的最小值作为第二个元素就和第三个元素比较
this.swap(i, min)
}
}
// 插入排序
insertionSort() {
// 从第一个元素开始,该元素可以认为已经被排序,从第二个元素开始比较
for (let i = 1; i < this.item.length; i++) {
let j = i;//第二个元素,索引为2
let mark = this.item[i];//每一次被标记的值设为未排序区间的首个数据,第一次就是第二个元素索引为1
while (this.item[j - 1] > mark && j > 0) {
// j-1是已经排好序区间的最大值当它大于被标记的值时就将被标记的值和排好序的区间依次进行比较,当j小于等于0时,j-1移动到了第一个元素之前就不会比较,所以j大于0需要一直向前依次比较
this.item[j] = this.item[j - 1];//和标记的值和已排好序区间的值进行比较
j--;//让比较的值向前移动
// 循环结束将被标记的值就是已排好序区间的最大值就放在j位置
}
// j-1是已经排好序区间的最大值当它小于被标记的值时,以及j-1移动到了第一个元素之前也不会比较就不用将被标记的值和排好序的区间依次进行比较,就直接将被标记的值放在j位置
this.item[j] = mark
}
}
// 快速排序
quicksort() {
this.item = this.quick(this.item)
}
quick(arr) {
if (arr.length <= 1) {
// 如果数组长度小于等于1,则直接返回
return arr;
} else {
// 分
let index = Math.floor(arr.length / 2);//基准在数组的索引
let middle = arr.splice(index, 1)[0];//从基准开始分割返回一个数组,数组只有一个元素就是基准
let leftArr = [],
rightarr = [];
for (let i = 0; i < arr.length; i++) {
// 比基准小的放在left,比基准大的放在right
if (arr[i] < middle) {
leftArr.push(arr[i]);
} else {
rightarr.push(arr[i]);
}
}
// 递归左数组和右数组
return this.quick(leftArr).concat(middle, this.quick(rightarr));
}
}
// 归并排序
mergerSort() {
this.item = this.divide(this.item);
}
// 分
divide(arr) {
if (arr.length <= 1) {
// 如果数组长度小于等于1,则直接返回数组,不需要再分割数组
return arr;
} else {
let index = Math.floor(arr.length / 2);//从数组中间开始分割数组
let leftArr = arr.slice(0, index);//右边不包含中间的元素
let rightArr = arr.slice(index);//左边是从中间的元素到数组最后一个元素
// 递归直到数组中只有一个元素再将这些数组进行合并后的结果返回
return this.merge(this.divide(leftArr), this.divide(rightArr))
}
}
// 和
merge(left, right) {
// 将分好的左右两边数组进行合并
let newarr = [];
// 每次left或者right都是要移除第一个元素
while (left.length && right.length) {
// 当左右数组其中一个为空即数组长度为0做布尔判断为false就不需要再进行比较直接将数组剩下的元素拼接到新数组
if (left[0] < right[0]) {
// 左边第一个元素小于右边第一个元素就将左边的第一个元素添加到新数组中,添加到新数组的元素需要从原数组中移除所以用shift方法
newarr.push(left.shift())
} else {
newarr.push(right.shift())
}
}
// 最后concat,因为不知道left或者right其中一个哪个会剩下元素,所以都进行将剩下的元素给拼接到新数组中
return newarr.concat(left, right)
}
// 顺序查找
shunxuSerach(ele) {
for (let i = 0; i < this.item.length; i++) {
if (this.item[i] == ele) {
// 找到了相同的元素就返回true
return true;
}
}
// 遍历结束也没有相同的值没找到返回false
return false;
}
// 二分法查找必须有序
BinarySearch(ele) {
let start = 0;//开始查找的位置
let end = this.item.length - 1;//结束查找的位置
if (ele < this.item[start] || ele > this.item[end]) {
// 当查找元素在有序的链表中比开始位置的最小元素小以及比最后一个位置的元素还要大就直接返回false不需要去查找,因为不可能存在在区间
return false;
}
while (start <= end) {
// 只要查找区间起始点和结束点中间还有值(包括两值相同的情况),就继续进行查找,否则结束查找
let middle = Math.floor((start + end) / 2);//查找的中间位置为开始和结束中间向下取整
if (ele < this.item[middle]) {
// 查找元素比middle位置的元素小需要在middle位置右边的区域查找,就将结束位置改变到middle位置的前一个,重新在这个新的位置区间范围查找
end = middle - 1
} else if (ele > this.item[middle]) {
// 查找元素比middle位置的元素大需要在middle位置左边的区域查找,就将开始位置改变到middle位置的后一个,重新在这个新的位置区间范围查找
start = middle + 1
} else {
// 当查找元素与middle位置的元素相等就表示找到了
return true;
}
}
// 所有元素都查找完也没有就返回false
return false;
}
}