在计算机中采用粗略的度量来描述计算机算法的效率,这种方法被称为“大O”表示法。
我们判断一个算法的效率,不能只凭着算法运行的速度,因为随着数据量的变化,算法的速度会发生变化,所以我们应该:
根据算法的速度随着数据量的变化会如何变化,这样的方式来表示算法的效率,大O表示法就是方式之一。
推导大O表示法:
规则一:用常量1取代运行时间中所有的加法常量。如7 + 8 = 15,用1表示运算结果15,大O表示法表示为O(1);
规则二:运算中只保留最高阶项。如N^3 + 3n +1,大O表示法表示为:O(N³);
规则三:若最高阶项的常数不为1,可将其省略。如4N2,大O表示法表示为:O(N²);
接下来是我们的集中排序算法:
简单排序:冒泡排序、选择排序、插入排序;
高级排序:希尔排序、快速排序;
我们封装一个列表来存储数据和排序算法
class ArrayList {
constructor() {
this.arr = []
}
insert(element) {
return this.arr.push(element);
}
toString() {
return this.arr.join(' ');
}
}
let list = new ArrayList();
list.insert(4);
list.insert(5);
list.insert(2);
list.insert(1);
list.insert(3);
console.log(list.toString());
我先自己写了一遍,我发现我写的这个其实是有问题的,内层循环控制两个元素依次比较,外层循环控制比较的趟数。这样写虽然能实现,但是你会发现其实内层循环每次都要比较arr.length-1
次,而实际上后面元素如果排好的话,根本不需要再比较了,比如21345,那么345就不用再比较了。
1.冒泡排序
bubbleSort() {
for(let i = 0; i < this.arr.length-1; i++) {
for(let i = 0; i < this.arr.length-1; i++) {
if(this.arr[i] > this.arr[i+1]) {
//交换两个位置的值
let zzy = this.arr[i+1];
this.arr[i+1] = this.arr[i];
this.arr[i] = zzy;
}
}
}
return this.arr
}
这样的话就需要进行一些小小的改进:
改进的就是这个for循环的次数,拿[4,2,1,3]
来举例,外层循环控制趟数,那么4个数比较3趟,依次递减(j=3第一趟,j=2第二趟,j=1第三趟),每一趟中都要两两比较,从下标为0开始,依次比较j
次(j=3第一趟比较3次,j=2第二趟比较2次,j=1第三趟比较1次)。
总结:4个数要比较三趟,第一趟比较3次,第二趟比较2次,第三趟比较1次
bubbleSort() {
for (var j = this.arr.length - 1; j > 0; j--) {
for (var i = 0; i < j; i++) {
if (this.arr[i] > this.arr[i + 1]) {
let zzy = this.arr[i + 1];
this.arr[i + 1] = this.arr[i];
this.arr[i] = zzy;
}
}
}
return this.arr
}
冒泡排序的效率:
上面所讲的对于7个数据项,比较次数为:6 + 5 + 4 + 3 + 2 + 1;
对于N个数据项,比较次数为:(N - 1) + (N - 2) + (N - 3) + … + 1 = N * (N - 1) / 2;
如果两次比较交换一次,那么交换次数为:N * (N - 1) / 4;
使用大O表示法表示比较次数和交换次数分别为:O(N*(N - 1)/2)
和O(N*(N - 1)/4)
,根据大O表示法的三条规则都化简为:O(N²);
选择排序的思路:从所有元素中找到最小的一个,和第一个元素交换位置;从后面元素中找到最小的一个,和第二个元素交换位置……,直到length-1
最后一个位置停止。
selectionSort() {
for (var j = 0; j < this.arr.length - 1; j++) {
let minIndex = j;
for (var i = minIndex + 1; i < this.arr.length; i++) {
if (this.arr[i] < this.arr[minIndex]) {
minIndex = i;
}
}
//交换两个位置元素
let temp = this.arr[minIndex];
this.arr[minIndex] = this.arr[j];
this.arr[j] = temp;
}
return this.arr;
}
还是拿[4,3,1,2]
举例吧,代码思路就是默认0位置
第一个数是最小的,然后内层循环从索引1
位置开始比较,找到最小数1
的索引2
后,和索引0
的4
交换([1,3,4,2]
);然后外层循环j=1
,下一层内层循环应该从索引2
处的4
开始比较,找到索引3处的2
,和索引1处的3交换
([1,2,4,3]
);然后外层循环j=2
,以此类推,直到外层循环走到倒数第二个位置。
选择排序的效率:
选择排序的比较次数为:N * (N - 1) / 2,用大O表示法表示为:O(N²);
选择排序每一趟交换一次,交换次数为:(N - 1) / 2,用大O表示法表示为:O(N);
所以选择排序的效率高于冒泡排序;
插入排序的思路:默认第一个数是有序的,然后前两个数变成有序,然后前三个数变成有序……也就是说第1趟比较前2个,第二趟比较前3个……如果比较时最后一个直接就是最大的,那么就break跳出循环。
拿[4,3,1,2,5]
举例,我画了个图
从上到下分别是每一趟,从左到右分别是每一次比较
//3.插入排序
insertionSort() {
//外层循环,从第一个位置开始获取数据。
for (let j = 1; j < this.arr.length; j++) {
//内层循环,从当前位置依次向前比较
for (let i = j - 1; i >= 0; i--) {
if (this.arr[i] > this.arr[j]) {
//交换位置
let temp = this.arr[j]; //保存当前数据
this.arr[j] = this.arr[i];
this.arr[i] = temp;
j--; //j往前挪
} else {
break; //如果比前边的数大,就退出循环
}
}
}
return this.arr;
}
插入排序的效率:
比较次数:第一趟最多比较1次;第二趟最多比较2次;以此类推,最后一趟最大为N-1;所以,插入排序的总比较次数为N * (N - 1) / 2;但是,实际上每趟发现插入点之前,平均只有全体数据项的一半需要进行比较,所以比较次数为:N * (N - 1) / 4;
交换次数也是同理:指一共需要交换N * (N - 1) / 2次,平均次数为N * (N - 1) / 4;
虽然用大O表示法表示插入排序的效率也是O(N²),但是插入排序整体操作次数更少(除非[5,4,3,2,1]
,不然不用比较和交换那么多次就排完了),因此,在简单排序中,插入排序效率最高;
1、先选取枢纽,这里选择第一个元素为枢纽(其实选择首尾和中间位置元素三者的中位数比较好,方法就是先把三个数取出来冒泡排序,然后找到中位数作为枢纽pivot)
2、定义左右两个指针,依次和枢纽比较,如果左边比枢纽大,左指针停,否则右移;如果右边比枢纽小,右指针停,否则向左移动;然后左右指针位置的数进行交换,直到左右指针相撞,那么所指位置的数就和枢纽交换。本次排序就完成了。
3、接下来递归调用,分别对枢纽左侧和枢纽右侧重复执行上述操作,直到满足某个条件开始出栈,最后返回arr
4、递归停止的条件:begin == end 此时说明枢纽左侧或右侧只有一个元素,不需要排序;还有begin > end 此时说明枢纽在第一个元素(begin=1,end=-1),枢纽左侧没东西了不需要递归了,左侧就出栈
function quickSort(arr, begin = 0, end = arr.length - 1) {
if(begin >= end) return; //递归开始出栈的条件
let i = begin; //定义一个左指针,指向第一个元素
let j = end; //定义一个右指针,指向最后一个元素
let pivot = arr[i]; //把枢纽设置为第一个元素
//当左右指针重合(i==j)时,交换当前指针元素和枢纽位置元素
while(i < j) {
//只要没有重合,就继续迭代指针
//先看右指针,如果右指针指向的数比pivot大,说明是正确的,继续向左查找
while(i < j && arr[j] >= pivot) {
//上面条件中,i < j 是为了防止指针迭代时i超过j,这是不对的
j--;
}
//再看左指针,如果左指针指向的数比pivot小,说明是正确的,继续向右查找
while(i < j && arr[i] <= pivot) {
i++;
}
//左右指针都停了,就交换两个指针的数
[arr[i],arr[j]] = [arr[j], arr[i]];
}
//指针碰撞,交换枢纽(我们设置的是begin位置)和当前位置
[arr[begin], arr[i]] = [arr[i], arr[begin]];
//,第一次排序结束,接下来左右分别递归
quickSort(arr, begin, i - 1); //左边递归,确定起始结束位置
quickSort(arr, i + 1, end); //右边递归,确定起始结束位置
}