一、大O表示法
简单排序:冒泡排序、选择排序、插入排序;
高级排序:希尔排序、快速排序、归并排序。
- 在计算机中采用粗略的度量来描述计算机算法的效率,这种方法被称为“大O”表示法。
- 在数据量(数据项个数)发生改变时,算法的效率也会跟着改变。所以说算法A比算法B快两倍,这样的比较是没有意义的。
- 因此我们根据算法的速度随着数据量的变化会如何变化的方式来表示算法的效率,大O表示法就是方式之一。
- 效率从大到小分别是:O(1)> O(logn)> O(n)> O(nlog(n))> O(n²)> O(2n)
推导大O表示法:
规则一:用常量1取代运行时间中所有的加法常量。如7 + 8 = 15,用1表示运算结果15,大O表示法表示为O(1);
规则二:运算中只保留最高阶项。如N^3 + 3n +1,大O表示法表示为:O(N³);
规则三:若最高阶项的常数不为1,可将其省略。如4N2,大O表示法表示为:O(N²);
封装一个列表ArrayList来存储数据和排序算法:
class ArrayList {
constructor() {
this.arr = []
}
insert(element) {
return this.arr.push(element);
}
toString() {
return this.arr.join(' ');
}
}
二、冒泡排序
冒泡排序的思路:
- 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系;
- 如果左边的人员高,则将两人交换位置。比如1比2矮,不交换位置;
- 向右移动一位,继续比较2和3,最后比较 length - 1 和 length - 2这两个数据;
- 当到达最右端时,最高的人一定被放在了最右边;
- 按照这个思路,从最左端重新开始时,只需要走到倒数第二个位置即可。
实现思路:两层循环
- 外层循环控制冒泡趟数:
- 第一次:j = length - 1,比较到倒数第一个位置 ;
- 第二次:j = length - 2,比较到倒数第二个位置 ;
- 内层循环控制每趟比较的次数:
- 第一次比较: i = 0,比较 0 和 1 位置的两个数据;
- 最后一次比较:i = length - 2,比较length - 2和 length - 1两个数据;
bubbleSort() {
for (let j = this.arr.length - 1; j >= 0; j--) {
for (let i = 0; i < j; i++) {
if (this.arr[i] > this.arr[i + 1]) {
let temp = this.arr[i];
this.arr[i] = this.arr[i + 1];
this.arr[i + 1] = temp;
}
}
}
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²);
三、选择排序
选择排序改进了冒泡排序:
- 将交换次数由O(N^2)减小到O(N);
- 但是比较次数依然是O(N^2);
选择排序的思路:
- 选定第一个索引的位置比如1,然后依次和后面的元素依次进行比较;
- 如果后面的元素,小于索引1位置的元素,则交换位置到索引1处;
- 经过一轮的比较之后,可以确定一开始指定的索引1位置的元素是最小的;
- 随后使用同样的方法除索引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;
}
选择排序的效率:
- 选择排序的比较次数为:N * (N - 1) / 2,用大O表示法表示为:O(N^2);
- 选择排序的交换次数为:(N - 1) / 2,用大O表示法表示为:O(N);
- 所以选择排序的效率高于冒泡排序;
四、插入排序
插入排序是简单排序中效率最高的一种排序。
插入排序的思路:
- 插入排序思想的核心是局部有序。如图所示,X左边的人称为局部有序;
- 首先指定一数据X(从第一个数据开始),并将数据X的左边变成局部有序状态;
- 随后将X右移一位,再次达到局部有序之后,继续右移一位,重复前面的操作直至X移至最后一个元素。
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;
- 交换次数:指定第一个数据为X时交换0次,指定第二个数据为X最多需要交换1次,以此类推,指定第N个数据为X时最多需要交换N - 1次,所以一共需要交换N * (N - 1) / 2次,平局次数为N * (N - 1) / 2;
- 虽然用大O表示法表示插入排序的效率也是O(N^2),但是插入排序整体操作次数更少,因此,在简单排序中,插入排序效率最高。
五、希尔排序
希尔排序是插入排序的一种高效的改进版,效率比插入排序要高。
插入排序的问题:
- 假设一个很小的数据项在很靠近右端的位置上,这里本应该是较大的数据项的位置;
- 将这个小数据项移动到左边的正确位置,所有的中间数据项都必须向右移动一位,这样效率非常低;
- 如果通过某种方式,不需要一个个移动所有中间的数据项,就能把较小的数据项移到左边,那么这个算法的执行速度就会有很大的改进。
希尔排序的实现思路:
- 希尔排序主要通过对数据进行分组实现快速排序;
- 根据设定的增量(gap)将数据分为gap个组(组数等于gap),再在每个分组中进行局部排序;
假如有数组有10个数据,第1个数据为黑色,增量为5。那么第二个为黑色的数据index=5,第3个数据为黑色的数据index = 10(不存在)。所以黑色的数据每组只有2个,10 / 2 = 5一共可分5组,即组数等于增量gap。
- 排序之后,减小增量,继续分组,再次进行局部排序,直到增量gap=1为止。随后只需进行微调就可完成数组的排序。
shellSort(arr) {
let len = arr.length;
for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < len; i++) {
let j = i;
let current = arr[i];
while (j - gap >= 0 && current < arr[j - gap]) {
arr[j] = arr[j - gap];
j = j - gap;
}
arr[j] = current;
}
}
return arr;
}
希尔排序的效率:
希尔排序的效率和增量有直接关系,即使使用原稿中的增量(N/2)效率都高于简单排序。
六、快速排序
- 快速排序可以说是目前所有排序算法中,最快的一种排序算法。当然,没有任何一种算法是在任意情况下都是最优的。但是,大多数情况下快速排序是比较好的选择。
- 快速排序其实是冒泡排序的升级版。
快速排序的核心思想是分而治之,先选出一个数据,将比其小的数据都放在它的左边,将比它大的数据都放在它的右边。
思路:
- 从数列中挑出一个元素,它称为 “基准”(pivot);
- 让数组中其他数字小于它的放在它的左边,让数组中其他数字大于它的放在它的右边,这个称为分区(partition)操作;
- 分别在左右两边的子数组中重复上述步骤,完成排序。
quickSort(arr, left, right) {
let len = arr.length,
partitionIndex,
left = typeof left != 'number' ? 0 : left,
right = typeof right != 'number' ? len - 1 : right;
if (left < right) {
partitionIndex = partition(arr, left, right);
quickSort(arr, left, partitionIndex-1);
quickSort(arr, partitionIndex+1, right);
}
return arr;
}
partition(arr, left ,right) { // 分区操作
let pivot = left, // 设定基准值(pivot)
index = pivot + 1;
for (let i = index; i <= right; i++) {
if (arr[i] < arr[pivot]) {
swap(arr, i, index);
index++;
}
}
swap(arr, pivot, index - 1);
return index-1;
}
swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
let quickSort = function(arr) {
if (arr.length <= 1) { return arr; }
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0];
let left = [];
let right = [];
for (let i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
//递归调用quickSort函数实现pivot左边数据left和右边数据right的排序;
};
快速排序的效率:
- 快速排序最坏情况下的效率:每次选择的枢纽都是最左边或最右边的数据,此时效率等同于冒泡排序,时间复杂度为O(n2)。可根据不同的枢纽选择避免这一情况;
- 快速排序的平均效率:为O(NlogN),虽然其他算法效率也可达到O(NlogN),但是其中快速排序是最好的。
七、归并排序
归并排序思路:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
mergeSort(arr) {
let len = arr.length;
if (len < 2) {
return arr;
}
let middle = Math.floor(len / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
merge(left, right) {
let result = [];
while (left.length>0 && right.length>0) {
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;
}