前几天小白接了一个小公司的电面,面试官上来就问你数据结构和算法怎么样啊,我说emmm还行,会基本的栈,队列的操作,会写基本的排序算法,他就说那你来讲一下快排吧,我就答emmm快排是用递归实现的,先要找到序列的枢轴点,然后左右递归,然后他追问怎么找呢,然后我就emmmmmm了,所以今天自罚手抄十大排序算法如下:
"use strict"
var arr = [];
var length = 100;
var max_value = 1000;
function swap(arr,index1, index2){ //交换函数
var temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
for(let i = 0; i < length; i++){
arr.push(Math.floor(max_value * Math.random()));
}
console.log(arr);
//以下排序为正序排序,遍历方式是从小的下标开始,以下函数均对全局函数arr数组进行排序
/*
选择排序:
接近人类正常思维的排序方法 即每一轮遍历选出最大值并放在开头的位置,下一轮遍历从剩下的元素开始
最好和最坏情况时间复杂度都为o(n^2),平均时间复杂度为o(n^2),空间复杂度为o(1),该算法不稳定
*/
function selectSort(){
for(let i = 0; i < length - 1; i++){
var min = i;
for(let j = min + 1; j < length; j++){
if(arr[j] < arr[min]){
min = j;//选出最小值的下标
}
}
swap(arr,i,min);//将最小值移动至开头
}
};
/*
冒泡排序:
顾名思义,每一轮排序相邻两个数互相比较,较大值浮向顶端,最终最大值到达顶端,下一轮排序浮出次大值,以此类推
最好情况时间复杂度为o(n),最坏情况时间复杂度为o(n^2),平均时间复杂度为o(n^2),空间复杂度为o(1),该算法稳定
*/
function bubbleSort(){
var jump = true;
for(let i = 0; i < length - 1; i++){ //如果上一次遍历没有发生交换说明剩余序列已经排好序了
var judge = false;
for(let j = 0; j < length - 1 - i; j++){
if(arr[j] > arr[j+1]){
swap(arr, j, j+1);
jump = false;
}
if(jump){
break;
}
}
}
};
/*
插入排序:
逐步构建有序序列的过程,每一轮排序都将当前数字插入到前面有序的序列。
最好情况时间复杂度为o(n),最坏情况时间复杂度为o(n^2),平均时间复杂度为o(n^2),空间复杂度为o(1),该算法稳定
另外tips:
折半插入排序(binary insertion sort)是对插入排序算法的一种改进,排序算法过程就是不断的依次将元素插入前面已排好序的序列中。
由于前半部分为已排好序的数列,这样我们不用按顺序依次寻找插入点,可以采用折半查找的方法来加快寻找插入点的速度。
折半查找只是减少了比较次数,但是元素的移动次数不变,所以时间复杂度为O(n^2)!此处不实现
*/
function insertSort(){
for(let i = 1; i < length; i++){
for(var j = i - 1, temp = arr[i]; j >= 0; j--){ //index为当前数字的下标
if(temp < arr[j]){
arr[j+1] = arr[j]; //将比当前数字大的数字往后移动
}
else{
break; //查找到插入点跳出循环
}
}
arr[j+1] = temp; //将当前数字插入到正确位置
}
};
/*
希尔排序:
1959年Shell发明,第一个突破O(n^2)的排序算法,是简单插入排序的改进版。
它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
平均时间复杂度为o(nlogn),空间复杂度为o(1),该算法不稳定
*/
function shellSort(){
function insertSort(step){
for(let i = step; i < length; i++){
for(var j = i - step, temp = arr[i]; j >= 0; j-=step){ //index为当前数字的下标
if(temp < arr[j]){
arr[j+step] = arr[j]; //将比当前数字大的数字往后移动
}
else{
break; //查找到插入点跳出循环
}
}
arr[j+step] = temp; //将当前数字插入到正确位置
}
}
var seq = [];
var temp = 1;
do{
seq.unshift(temp);
temp = 3 * temp + 1;
}while(temp < length); //生成Knuth 增量序列
seq.forEach(item => insertSort(item));//根据步长进行多次排序,最后一次步长为1,也即正常的插入排序
};
/*
归并排序:
归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
归并排序是建立在归并操作上的一种有效的排序算法,将已有序的子序列合并,得到完全有序的序列
该算法是采用分治法的一个非常典型的应用。归并排序是一种稳定的排序方法。
*/
function mergeSort(arr){ //归并排序的递归实现
function merge(arr1, arr2){
var arr3 = []; //将arr1,arr2归并为arr3
for(var index1 = 0, index2 = 0; index1 < arr1.length && index2 < arr2.length;){
if(arr1[index1] <= arr2[index2]){
arr3.push(arr1[index1]);
index1++;
}
else{
arr3.push(arr2[index2]);
index2++;
}
}
for(let i = index1; i < arr1.length; i++){
arr3.push(arr1[i]);
}
for(let i = index2; i < arr2.length; i++){
arr3.push(arr2[i]);
}
/*将arr1或arr2剩余的元素添加到末尾*/
return arr3;
};
if(arr.length >= 2){
var middle = Math.floor(arr.length / 2),
left = arr.slice(0, middle),
right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));//将两个子序列排序后合并
}
else{
return arr; //无需再分治
}
return arr;
}
/*
快速排序:
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,
其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n^2) 平均情况:T(n) = O(nlogn)
(题外话:晚上在宿舍跟舍友讨论快排算法时间复杂度的稳定性,我问舍友快排时间复杂度为O(n^2)的概率是多少,
舍友说概率和我大三能脱单的概率差不多...emmmm差点想抄起椅子打人)
*/
function quickSort(arr,left,right){
var pivot;
if(left < right){
pivot = partition(arr, left, right);
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
function partition(arr, left, right){
var pivot = left, //选择枢轴的值,这里选第一个值为枢轴
left_index = left ,
right_index = right,
dir = true; //遍历方向,true代表从左到右
while(left_index <= right_index){ //当左右指针相遇的时候遍历结束
if(dir){
if(arr[pivot] > arr[right_index]){
swap(arr, pivot, right_index);
pivot = right_index;
left_index++;
dir = !dir; //转向遍历
}
else{
right_index--;
}
}
else{
if(arr[pivot] < arr[left_index]){
swap(arr, pivot, left_index);
pivot = left_index;
right_index--;
dir = !dir; //转向遍历
}
else{
left_index++;
}
}
}
return pivot;
}
}
/*
堆排序:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。
堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序的基本思想是:
将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
将其与末尾元素进行交换,此时末尾就为最大值。
然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
如此反复执行,便能得到一个有序序列了
*/
function heapSort(){
function adjustHeap(index){
var lchild = 2 * index + 1, //左子节点的下标
rchild = 2 * index + 2, //右子节点的下标
max_child;
if(len > rchild){ //右子节点在待排序序列中
max_child = arr[rchild] > arr[lchild] ? rchild : lchild;
}else if(len > lchild){
max_child = lchild;
}else{
max_child = index;
}
if(arr[index] < arr[max_child]){
swap(arr, index, max_child);
adjustHeap(max_child); //通过递归实现调整二叉树恢复为最大堆
}
}
function buildheapSort(){
for(let i = len / 2 - 1; i >= 0; i--){ //(len / 2 - 1) 为最大堆的最后一个非叶子节点的下标
adjustHeap(i); //从二叉树由上到下逐步构建最大堆。
}
}
var len = arr.length //函数的局部变量,表示当前待排序序列的长度
buildheapSort();
for(let i = len - 1; i > 0; i--){
swap(arr, 0, i);
len--;
adjustHeap(0);
}
}
/*
计数排序:
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。
作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
计数排序(Counting sort)是一种稳定的排序算法。它只能对整数进行排序。
最佳情况:T(n) = O(n+k) 最差情况:T(n) = O(n+k) 平均情况:T(n) = O(n+k) (n为数字量,k为范围)
*/
function countSort(){
var array = new Array(); //存储了下标代表的数字出现的次数
var sortArray = new Array(); //排序后的数组
arr.forEach(item => {
array[item] = array[item] ? (array[item] + 1) : 1;
});
for(let i = 0; i < array.length; i++){
for(let j = 0; j < array[i]; j++){
sortArray.push(i);
}
}
return sortArray;
}
/*
桶排序:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,
每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
*/
function bucketSort(arr, bucketSize) {
if (arr.length === 0) {
return arr;
}
var i;
var minValue = arr[0];
var maxValue = arr[0];
for (i = 1; i < arr.length; i++) {
if (arr[i] < minValue) {
minValue = arr[i]; //输入数据的最小值
} else if (arr[i] > maxValue) {
maxValue = arr[i]; //输入数据的最大值
}
}
//桶的初始化
var DEFAULT_BUCKET_SIZE = 5; //设置桶的默认数量为5
bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;
var buckets = new Array(bucketCount);
for (i = 0; i < buckets.length; i++) {
buckets[i] = [];
}
//利用映射函数将数据分配到各个桶中
for (i = 0; i < arr.length; i++) {
buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
}
arr.length = 0;
for (i = 0; i < buckets.length; i++) {
insertionSort(buckets[i]); //对每个桶进行排序,这里使用了插入排序
for (var j = 0; j < buckets[i].length; j++) {
arr.push(buckets[i][j]);
}
}
return arr;
}
/*
基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。
最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以是稳定的。
基数排序法是属于稳定性的排序,也是非比较的排序算法
复杂度为O(kn),n为数组长度,k为数组中的数的最大的位数,最佳情况和最差情况:T(n) = O(n * k)
*/
function radixSort(radix, maxDigit){ //radix表示基数的值,maxDigit表示在该基数下数组的最大值的位数
if(typeof radix != 'number' || radix < 2 || typeof maxDigit != 'number' || maxDigit < 1){
console.log(typeof radix);
return;
}
var queue = new Array(radix); //存放一遍基数排序结果的队列
for(let i = 0; i < radix; i++){
queue[i] = new Array();
}
for(let i = 1; i <= maxDigit; i++){
var divisor = Math.pow(radix, i - 1);
arr.forEach((item, index) => {
var place = Math.floor(item / divisor) % radix;
queue[place].push(item);
});
arr.length = 0; //清空排序数组,重新存放新一轮结果
for(let i = 0; i < radix; i++){
for(let j = 0; j < queue[i].length; j++){
arr.push(queue[i][j]);
}
queue[i].length = 0;//清空队列
}
}
}
参考资料: https://www.cnblogs.com/onepixel/articles/7674659.html