JS-经典排序算法
冒泡排序
冒泡排序是最为基础的一种排序算法,也是我们学习的第一个排序算法,算法的基本原理是在待排数组中通过遍历所有元素并交换相邻元素的位置,使得最大(或最小)数能够慢慢移动到数组的顶端(或尾部),整个过程就好像水泡慢慢浮出水面,冒泡因此得来。
冒泡排序算法的基本流程:
- 比较相邻元素,如果前面的元素大于后面的元素,就把它们交换位置。
- 对每一对相邻的元素执行步骤一,遍历一次后发现最大的元素被移动到了数组末端。
- 除了最后一个元素,继续遍历整个数组,重复上面操作直到没有元素需要被交换。
JS代码如下:
function bubbleSort(arr){
for(var i=0;i<arr.length-1;i++){
for(var 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;
}
冒泡排序是一种稳定的排序,因为在比较时,我们只对前一个元素大于后面元素时进行操作,对于相等元素并不进行交换;冒泡排序最好情况下的时间复杂度为O(n),最坏情况下的时间复杂度为O(n^2),空间复杂度为O(1)。
冒泡排序的改进—鸡尾酒排序
它和冒泡排序的不同之处在于它在遍历时采取一前一后的遍历方式,即第一次从前往后遍历找出最大值扔到数组末尾,第二次从后往前遍历找到最小值扔到数组顶端,这种排序算法在遇到已经基本排序的数组比如(2,3,4,5,1),只需要前后各一次遍历便排好了整个数组,无需像冒泡排序一样需要遍历多次。
JS代码如下:
//冒泡改进--鸡尾酒排序
function cocktailSort(arr){
var left=0,right=arr.length-1;
while(left<right){
for(var i=left;i<right;i++){
if(arr[i]>arr[i+1]){
var temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
}
right--;
for(var j=right;j>left;j--){
if(arr[j]<arr[j-1]){
var temp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = temp;
}
}
left++;
}
return arr;
}
其时间复杂度在已基本排好序的数组中可接近于O(n),在大量无序的数组中效率和冒泡一样差。
插入排序
类似于我们在玩扑克牌时对牌进行的排序,从头开始,不断地把无序的牌插入到已排好序的牌中,直到所有牌排序完成。
插入排序流程:
- 从头开始,默认第一个元素已排好序,然后从已排好序的序列的末端开始向前遍历,找到一个比它小的元素,插到后面。
- 对未排序的序列重复上面的操作直到所有元素排好序。
JS代码如下:
function insertSort(arr){
for(var i=1;i<arr.length;i++){
if(arr[i-1]>arr[i]){
var temp = arr[i];
var j=i;
while(j>=0&&arr[j-1]>temp){
arr[j]=arr[j-1];
j--;
}
arr[j]=temp;
}
}
return arr;
}
插入排序是一种稳定的排序,因为它是对已排序好序的后面往前遍历,在遇到相同元素时,不会继续往前走,保持了原来的次序,所以稳定;插入排序最好情况下的时间复杂度为O(n),最坏情况下的时间复杂度为O(n^2),,空间复杂度是O(1)。
二分插入排序插入排序的改进
如果比较的操作远大于交换的操作时,我们可以采取二分查找的方式来减小比较次数。
JS代码如下:
//采用二分查找减小比较次数
function insertionSortDichotomy(arr){
for(var i=1;i<arr.length;i++){
var temp = arr[i];
var left=0,right=i-1,mid=0;
while(left<=right){
mid = Math.round((left+right)/2);
if(arr[i]<arr[mid]){
right = mid - 1;
}
else{
left = mid + 1;
}
}
for(var j=i;j>left;j--){
arr[j] = arr[j-1];
}
arr[left] = temp;
}
return arr;
}
改进后的插入排序的最坏情况下事件复杂度为O(nlogn)。
希尔排序(插入排序的更高级改进)
希尔排序,又叫递减增量排序,是插入排序的一种更高效的改进版本,希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法的最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已排好的了(此时插入排序较快)。
希尔排序流程:
- 通过函数h=h*3+1来计算步长,首先找出最大步长。
- 根据最大步长分组并使用插入排序,即每隔一定步长取一个数,对取的数进行插入排序。
- 利用h=(h-1)/3改变步长,重复上面的操作。
JS代码如下:
function shellSort(arr){
var step=0;
while(step<arr.length){
step = step*3 + 1;
}
while(step>=1){
for(var i=step;i<arr.length;i++){
var temp = arr[i];
var j = i-step;
while(j>=0&&arr[j]>temp){
arr[j+step] = arr[j];
j = j - step;
}
arr[j+step] = temp;
}
step = (step-1)/3;
}
return arr;
}
希尔排序是一种不稳定的排序算法,因为它在交换时,间隔一定步长的数,导致可能越过相同的数到达前面;例如(5,3,2,5,4),步长为4,当5和4交换后,第一个5位于第二个5后面;希尔排序的时间复杂度和步长序列有关,目前最好步长算法的最坏情况的复杂度是O(n(logn)^2),最好的情况下的时间复杂度是O(n),空间复杂度是O(1)。
选择排序
选择排序,,顾名思义,每次遍历数组,找出最小(或最大)数,放到数组顶部(或末端),然后从剩余的数组里选择最小(或最大)数,放入第二个位置,直到所有数顺序。
JS代码如下:
function selectSort(arr){
for(var i in arr){
var min = arr[i],minIndex = i;
for(var j = i;j<arr.length;j++){
if(arr[j]<min){
min = arr[j];
minIndex = j;
}
}
if(minIndex!=i){
var temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
选择排序是一种不稳定的排序,因为当arr[i]和最小数交换时,可能会把相同数的次序打乱,比如序列{ 5, 8, 5, 2, 9 },一次选择的最小元素是2,然后把2和第一个5进行交换,从而改变了两个元素5的相对次序。选择排序的最好情况的时间复杂度是O(n^2),最坏情况的时O(n^2),空间复杂度是O(1)。
归并排序
归并排序采取的是一种叫做分治法的思想,将一个大问题分割成小问题分别解决,然后用所有小问题的答案来解决整个大问题,归并排序算法主要依赖归并(Merge)操作。归并操作指的是将两个已经排序的序列合并成一个序列的操作。
归并排序的流程:
- 申请一片新的空间用来保存合并后的数组。
- 从头开始比较两个数组,将较小的数push到开辟的空间内,同时删除这个数。
- 对要排序的数组进行分割操作,分成左右两部分,同时递归该过程,并对分割的两部分进行归并操作。
实例解析:
JS代码如下:
function merge(left,right){
var temp = [];
while(left.length&&right.length){
if(left[0]>right[0]){
temp.push(right.shift());
}else{
temp.push(left.shift());
}
}
return temp.concat(left).concat(right);
}
function mergeSort(arr){
if(arr.length==1) return arr;
var left=[],right=[],mid=0;
mid = Math.floor(arr.length/2);
left = arr.slice(0,mid);
right = arr.slice(mid);
return merge(mergeSort(left),mergeSort(right));
}
归并排序是一种稳定的排序,最好情况的时间复杂度是O(nlogn),最坏情况的时间复杂度是O(nlogn),空间复杂度是O(n)。
快速排序
快速排序和归并排序一样,使用分治策略(Divide and Conquer),把一个序列通过基准分为两个子序列。等到分到最后为空或者只有一个数时,必然是有序的,这样合并后的序列整体也是有序的,事实上,快速排序通常明显比其他O(nlogn)算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。
快速排序的流程:
- 从序列中挑出一个元素,作为”基准”(pivot)。
- 把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
- 对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
JS代码如下:
function quickSort(arr){
if(arr.length<=1) return arr;
var pivot = Math.floor(arr.length/2);
var pivotVal = arr.splice(mid,1)[0];
var left=[],right=[];
for(var i=0;i<arr.length;i++){
if(arr[i]<pivotVal){
left.push(arr[i]);
}else{
right.push(arr[i]);
}
}
return quickSort(left).concat(pivotVal,quickSort(right));
}
快速排序显然是一种不稳定的排序,不稳定发生在元素与基准元素交换的时刻。比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基准元素是5,一次划分操作后5要和第一个8进行交换,从而改变了两个元素8的相对次序。快速排序的最坏情况时间复杂度是O(n^2),最好情况是O(nlogn),平均时间复杂度是O(nlogn),空间复杂度取决于递归树的深度,一般为O(logn),最差为O(n) 。
堆排序
堆排序,是一种利用了堆这种数据结构的选择排序,堆是一种近似于完全二叉树的结构,通常有大根堆和小根堆,拿大根堆来说,大根堆的父节点一定大于或等于它的子节点。
堆排序流程:
- 把初始的无序数组构建成一个大根堆,需要自己写一个调整堆的算法,具体是传入父节点,比较两个孩子,把最大值和父节点交换,递归调整所有的子节点,然后从arr.length/2处开始建堆。
- 交换首尾的值,从堆中移除队尾元素,调整堆。
- 最后得到的数组已经是顺序的了。
JS代码如下:
function buildMaxHeap(arr){
for(var i=Math.floor(arr.length/2);i>=0;i--){
heapify(arr,i);
}
}
function swap(a,x,y){
var temp = a[x];
a[x] = a[y];
a[y] = temp;
}
function heapify(arr,ans,size){
var left = ans*2+1;//左孩子
var right = ans*2+2;//右孩子
var largest = ans;
if(left<size&&arr[left]>arr[largest]){
largest = left;
}
if(right<size&&arr[right]>arr[largest]){
largest = right;
}
if(ans!=largest){
swap(arr,ans,largest);
heapify(arr,largest,size);
}
}
function heapSort(arr){
buildMaxHeap(arr);
for(var i=arr.length-1;i>0;i--){
swap(arr,0,i);
heapify(arr,0,i);
}
return arr;
}
堆排序是一种不稳定的排序,不稳定发生在堆顶元素和末尾交换的时候,比如{ 9, 5, 7, 5 },堆顶元素是9,堆排序下一步将9和第二个5进行交换,得到序列 { 5, 5, 7, 9 },再进行堆调整得到{ 7, 5, 5, 9 },重复之前的操作最后得到{ 5, 5, 7, 9 }从而改变了两个5的相对次序;堆排序的最好情况的时间复杂度是O(nlogn),最差情况也是O(nlogn);空间复杂度是O(1)。
计数排序
计数排序,使用一个额外的数组C来记录arr数组中每个元素出现的个数,然后计算并使用额外数组C小于等于arr中元素的个数,类似于有10个人,我们要按年龄排序,我们知道小明的年龄大于等于8个人,则小明就排在第8位,为确保稳定性,我们反向填充目标数组,填充完毕后将对应的数字统计递减。
计数排序流程:
- 开辟一个新数组C用来记录arr中每个元素的个数。
- 对数组C中的元素与其前一项相加,得到小于等于arr[i]的个数。
- 反向填充到数组B中,每次填充完后,对应的计数减一。
JS代码如下:
const k = 100;//排序的数组最大不超过100
function countingSort(arr){
var temp = [],ans=[];
for(var j=0;j<k;j++){
temp[j]=0;
}
for(var i in arr){
temp[arr[i]]++;
}
for(var m=1;m<k;m++){
temp[m] = temp[m] + temp[m-1];
}
for(var n=arr.length-1;n>=0;n--){
ans[--temp[arr[n]]] = arr[n];
}
return ans;
}
计数排序是一种稳定的排序,它的最好情况下时间复杂度是O(n + k),最坏情况的是O(n + k),空间复杂度是O(n + k),值得注意的是,计数排序的时间复杂度和空间复杂度与数组A的数据范围(A中元素的最大值与最小值的差加上1)有关,因此对于数据范围很大的数组,计数排序需要大量时间和内存。
基数排序
基数排序,将所有待比较正整数统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始进行基数为10的计数排序,一直到最高位计数排序完后,数列就变成一个有序序列(利用了计数排序的稳定性)。
下图给出了对{ 329, 457, 657, 839, 436, 720, 355 }进行基数排序的简单演示过程:
JS代码如下:
const dn = 1;//排序数组的最大位数
const k = 10;//基数为10
function getDigit(num,d){
var radix = [1,10,100,1000,10000];//假设最大位数为5
return num/radix[d]%10;
}
function countingSort(arr,d){
var temp = [],ans=[];
for(var j=0;j<k;j++){
temp[j]=0;
}
for(var i in arr){
temp[getDigit(arr[i],d)]++;
}
for(var m=1;m<k;m++){
temp[m] = temp[m] + temp[m-1];
}
for(var n=arr.length-1;n>=0;n--){
ans[--temp[getDigit(arr[n],d)]] = arr[n];
}
for(var l in arr){
arr[l] = ans[l];
}
}
function LsdRadixSort(arr){
for(var i=0;i<dn;i++){
console.log(arr);
countingSort(arr,i);
//console.log(arr);
}
return arr;
}
基数排序是一种稳定的排序,因为它是基于计数排序的,基数排序的时间复杂度是O(n * dn),其中n是待排序元素个数,dn是数字位数。这个时间复杂度不一定优于O(n log n),dn的大小取决于数字位的选择(比如比特位数),和待排序数据所属数据类型的全集的大小;dn决定了进行多少轮处理,而n是每轮处理的操作数目。空间复杂度也是O(n * dn)。
如果考虑和比较排序进行对照,基数排序的形式复杂度虽然不一定更小,但由于不进行比较,因此其基本操作的代价较小,而且如果适当的选择基数,dn一般不大于log n,所以基数排序一般要快过基于比较的排序,比如快速排序。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序并不是只能用于整数排序。
桶排序
桶排序也叫作箱排序,工作的原理是将数组元素映射到有限数量个桶里,利用计数排序可以定位桶的边界,每个桶再各自进行桶内排序(使用其它排序算法或以递归方式继续使用桶排序)。
JS代码如下:
const bn = 5; // 这里排序[0,49]的元素,使用5个桶就够了,也可以根据输入动态确定桶的数量
var C = []; // 计数数组,存放桶的边界信息
function insertionSort(arr,left,right)
{
for (var i = left + 1; i <= right; i++) // 从第二张牌开始抓,直到最后一张牌
{
var get = arr[i];
var j = i - 1;
while (j >= left && arr[j] > get)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = get;
}
}
function mapToBucket(x)
{
return Math.floor(x / 10); // 映射函数f(x),作用相当于快排中的Partition,把大量数据分割成基本有序的数据块
}
function countingSort(arr)
{
for (var i = 0; i < bn; i++)
{
C[i] = 0;
}
for (var i = 0; i < arr.length; i++) // 使C[i]保存着i号桶中元素的个数
{
C[mapToBucket(arr[i])]++;
}
for (var i = 1; i < bn; i++) // 定位桶边界:初始时,C[i]-1为i号桶最后一个元素的位置
{
C[i] = C[i] + C[i - 1];
}
var B = [];
for (vari = arr.length - 1; i >= 0; i--)// 从后向前扫描保证计数排序的稳定性(重复元素相对次序不变)
{
var b = mapToBucket(arr[i]); // 元素A[i]位于b号桶
B[--C[b]] = arr[i]; // 把每个元素A[i]放到它在输出数组B中的正确位置上
// 桶的边界被更新:C[b]为b号桶第一个元素的位置
}
for (var i = 0; i < arr.length; i++)
{
arr[i] = B[i];
}
}
function bucketSort(arr)
{
countingSort(arr); // 利用计数排序确定各个桶的边界(分桶)
for (var i = 0; i < bn; i++) // 对每一个桶中的元素应用插入排序
{
var left = C[i]; // C[i]为i号桶第一个元素的位置
var right = (i == bn - 1 ? arr.length - 1 : C[i + 1] - 1);// C[i+1]-1为i号桶最后一个元素的位置
if (left < right) // 对元素个数大于1的桶进行桶内插入排序
insertionSort(arr, left, right);
}
return arr;
}
桶排序是一种稳定的排序,因为我们使用的计数排序和插入排序都是稳定的,最差时间复杂度 O(nlogn)或O(n^2),只有一个桶,取决于桶内排序方式,最优时间复杂度O(n),每个元素占一个桶,平均时间复杂度 O(n),保证各个桶内元素个数均匀即可,所需辅助空间 O(n + bn)。