排序算法
冒泡排序
冒泡排序算法是把较小的元素往前调或者把较大的元素往后调。这种方法主要是通过对相邻两个元素进行大小的比较,根据比较结果和算法规则对该二元素的位置进行交换,这样逐个依次进行比较和交换,就能达到排序目的。
冒泡排序基本思想:
- 较小的元素往前调或者把较大的元素往后调
- 对相邻两个元素的比较,两元素相同则不需要交换位置
- 比较轮数为数据长度减1,即结束位置为倒数第二个数据
- 每一轮的比较次数为数据长度减1减去比较轮数(也可以理解为已经排好序数据的数量)
代码实现:
function maopao(arr) {
//外层循环表示排序的次数
for (let i = 0; i < arr.length - 1; i++) {
//内层循环表示每次比较的数据
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
var temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return arr;
}
console.log(maopao([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析;
- 平均时间复杂度:O(N^2)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
选择排序
选择排序算法的基本思路是为每一个位置选择当前最小的元素。它是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间。
PS:使用这种排序时,要注意其中一个不同于冒泡法的细节。在**排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序。**举例说明:序列58539.我们知道第一遍选择第1个元素“5”会和元素“3”交换,那么原序列中的两个相同元素“5”之间的前后相对顺序就发生了改变。因此,我们说选择排序不是稳定的排序算法,它在计算过程中会破坏稳定性。
选择排序基本思想:
- 从第一个位置开始找出元素中最小(大)的数与之替换位置,再跳到下一个位置继续查找最小(大)的数与之替换…
- 结束位置为遍历到元素中倒数第二个,此时最后一个即为最大(小)的数字
代码实现:
function xuanze(arr) {
for (let i = 0; i < arr.length - 1; i++) {
var min = i;
for (let j = min + 1; j < arr.length; j++) {
if (arr[min] > arr[j]) {
min = j;
}
}
console.log("当前排序位置的最小值的索引值" + min);
let temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
return arr;
}
console.log(xuanze([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析:
- 平均时间复杂度:O(N^2)
- 最佳时间复杂度:O(N^2)
- 最差时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
插入排序
插入排序算法是基于某序列已经有序排列的情况下,通过一次插入一个元素的方式按照原有排序方式增加元素。这种比较是从该有序序列的最末端开始执行,即要插入序列中的元素最先和有序序列中最大的元素比较,若其大于该最大元素,则可直接插入最大元素的后面即可,否则再向前一位比较查找直至找到应该插入的位置为止。
插入排序的基本思想是:
- 每次将1个待排序的记录按其关键字大小插入到前面已经排好序的子序列中,寻找最适当的位置,直至全部记录插入完毕。
- 执行过程中,若遇到和插入元素相等的位置,则将要插人的元素放在该相等元素的后面,因此插入该元素后并未改变原序列的前后顺序。所以我们认为插入排序也是一种稳定的排序方法。
- 插入排序分直接插入排序、折半插入排序和希尔排序3类。
直接插入排序
直接插入排序基本思想:
- 将待排序的数据看成一个无序表和一个有序表
- 将无序表的数据依次插入到有序表当中的适当位置(即大于前一个数,小于后一个数的位置
直接插入排序的实现步骤:
- 从第一个元素开始,该元素可以认为有序表中的第一个数
- 取出下一个元素,在已经排序的元素序列中从后向前扫描,如果该元素(已排序)大于新元素,将该元素移到下一位置
- 重复上一步,直到找到已排序的元素小于或者等于新元素的位置,将新元素插入到该位置后
- 重复2,3
代码实现:
function insertSort(arr) {
let length = arr.length;
//从第一个位置开始获取数据,插入有序序列
for (let i = 1; i < length; i++) {
//获取i位置的元素,和前面的数据依次进行比较
let temp = arr[i];
let j = i;
//如果从后开始遍历有序序列,插入元素小于有序序列元素,则将所有大于插入元素的元素往后移
while (arr[j - 1] > tmp && j > 0) {
arr[j] = arr[j - 1];
j--;
}
//将j位置的数据插入到temp
arr[j] = temp;
}
return arr
}
console.log(insertSort([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析:
- 平均时间复杂度:O(N^2)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
折半插入排序
折半插入排序是对直接插入排序算法的一种改进,由于排序算法过程中,就是不断的依次将元素插入前面已排好序的序列中。由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
折半插入排序的基本思路:
在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high],则每轮比较时将待插入元素与a[m],其中m=(low+high)/2相比较,如果比参考元素小,则选择a[low]到a[m-1]为新的插入区域(即high=m-1),否则选择a[m+1]到a[high]为新的插入区域(即low=m+1),如此直至low<=high不成立,即将此位置之后所有元素后移一位,并将新元素插入a[high+1]。
代码实现:
function zheban(arr) {
for (let i = 1; i < arr.length; i++) {
let left = 0;
// 是在第 i 项的左面查找出合适的位置将 第 i 项插入进去,所以查找范围是 第 i 项的左侧
// 所以 right 为 i-1
let right = i - 1;
// 因为插入元素要移动插入位置之后的数组元素,所以先保存要插入的元素,防止被覆盖掉
const temp = arr[i];
// 二分查找算法
// left的值就是要插入的下标位置
while (left <= right) {
let mid = Math.floor((left + right) / 2);
console.log(left,right,mid);
if (arr[mid] > arr[i]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 从二分查找的那部分数组的末尾,到要插入的位置全部向后移动一位
for (let j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
// 插入
arr[left] = temp;
}
return arr;
}
console.log(zheban([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析:
- 平均时间复杂度:O(N^2)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
希尔排序
希尔排序基本思路:
- 将无序数组分割成若干子序列,子序列不是逐段分割的,而是相隔特定增量的子序列,对各个子序列进行插入排序
- 然后再选择一个更小的增量,再将之前排序后的数组按这个增量分割成多个子序列
- 不断选择更小增量,直到增量为1时,再对序列进行一次插入排序,使序列最终成为有序序列,排序完成。
即希尔排序的关键是:确认一个有规律的逐渐递减的增量,这里的首选增量为n/2,每次增量为原先的1/2,直到增量为1
代码实现:
function xier(arr) {
let n = arr.length;
//确定增量的变化,将数组分为多少份
for (let gap = n / 2; gap > 0; gap = Math.floor(gap / 2)) {
//对每一组的数据进行比较
for (let i = gap; i < n; i++) {//获取到当前组别
for (let j = i - gap; j >= 0; j -= gap) {//获取同一组的前一个数
//如果同一组靠前的数大于靠后的数,则置换位置
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
return arr;
}
console.log(xier([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析:
- 平均时间复杂度:O(nlog2n)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(nlog2n)
- 空间复杂度:O(1)
- 稳定性:稳定
归并排序
归并排序算法就是把序列递归划分成为一个个短序列,以其中只有1个元素的直接序列或者只有2个元素的序列作为短序列的递归出口,再将全部有序的短序列按照一定的规则进行排序为长序列。需要注意的是,在进行元素比较和交换时,若两个元素大小相等则不必刻意交换位置,因此该算法不会破坏序列的稳定性,即归并排序也是稳定的排序算法。
代码实现:
function merge(leftArr, rightArr) {
let leftL = leftArr.length;
let rightL = rightArr.length;
let result = [];
let i=0;
let j = 0;
while (i < leftL && j < rightL) {
leftArr[i] < rightArr[j] ? result.push(leftArr[i++]) : result.push(rightArr[j++]);
}
while (i < leftL) {
result.push(leftArr[i++])
}
while (j < rightL) {
result.push(rightArr[j++])
}
return result
}
function guibing(arr) {
let n = arr.length;
if (n <= 1) {
return arr;
}
let mid = Math.floor(n / 2);
let left = arr.slice(0, mid);
let right = arr.slice(mid);
return merge(guibing(left), guibing(right))//合并左右部分
}
console.log(guibing([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]
性能分析:
- 平均时间复杂度:O(nlogn)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(nlogn)
- 空间复杂度:O(N)
- 稳定性:稳定
快速排序
快速排序的基本思想是:通过一趟排序算法把所需要排序的序列的元素分割成两大块,其中,一部分的元素都要小于或等于另外一部分的序列元素,然后仍根据该种方法对划分后的这两块序列的元素分别再次实行快速排序算法。排序实现的整个过程可以是递归的来进行调用,最终能够实现将所需排序的无序序列元素变为一个有序的序列。就快速排序同样也是不稳定算法:排序之前,有两个数相等,但是在排序结束之后,它们两个有可能改变顺序。
代码实现:
function kuaisu(arr) {
if (arr.length <= 1) return arr;
const num = arr[0];
let left = [], right = [];
for (let i = 1; i < arr.length; i++) {
arr[i] <= num ? left.push(arr[i]) : right.push(arr[i]);
}
console.log("left为:" + left, "right为:" + right);
return kuaisu(left).concat([num], kuaisu(right));
}
console.log(kuaisu([2, 1, 4, 6, 1, 2, 7, 3]));//[1, 1, 2, 2, 3, 4, 6, 7]第一个1和第二个1在排序前后的位置不一样
性能分析:
- 平均时间复杂度:O(nlogn)
- 最佳时间复杂度:O(N)
- 最差时间复杂度:O(nlog2n)
- 空间复杂度:O(logn)
- 稳定性:不稳定