排序算法是一种能将一串资料依照特定排序方式进行排列的算法
排序算法的输出必须遵守的原则
输出结果为递增序列(递增是针对所需的排序顺序而言)
输出结果是原输入的一种排列、或是重组
关于算法的相关概念
稳定排序 如果a原本在b的前面,且a=b,排序之后a仍然在b的前面,则为稳定排序
非稳定排序 如果a原本在b的前面,且a=b,排序之后a可能不在b的前面,则为非稳定排序
原地排序 指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的排序
非原地排序 需要利用额外的数组来辅助排序
时间复杂度 一个算法执行所消耗的时间
空间复杂度 运行完一个算法所需的内存大小
快速排序(quickSort)
快速排序是分治的算法,核心是分区,每次选择一个元素为基准,并且将整个数组以基准分为两部分
比基准小的放基准前面,比基准大的放基准后面;基准的位置就是正确的,因为前面的都比它小,后面的都比它大
然后使用递归的把小于基准和大于基准的分区进行排序
时间复杂度:平均O(n log n) 最坏O(n^2) 空间复杂度O(log n)
代码实现
let quickSort = arr =>{
if(arr.length <= 1){
return arr;
}
let pivotIndex = Math.floor(arr.length/2); // 声明基准,数组长度除2
let pivot = arr.splice(provtIndex, 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)])
}
计数排序(countSort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中
实现思路,使用一个哈希表做记录,发现数字N,就记做N:1,如果再次发现N就加1
最后把哈希表的key全部打出来,假设N:M,那么N就打印M次
时间复杂度:平均O(n+m) 空间复杂度O(n+m)
代码实现
let countSort = arr =>{
let hashTable = {}, max = 0, result = []; // 声明哈希,最大值,存放排好序的数组
for(let i=0; i<arr.length; i++){ // 遍历数组
if(!(arr[i] in hashTable)){ // 如果数组的第i项不在哈希里,哈希的第i项=1
hashTable[arr[i]] = 1 // 指arr[1]出现的次数为1次
}else{
hashTable[arr[i]] += 1 // 如果存在,就把arr[i]的次数加1
}
if(arr[i] > max){ // 记录最大数,收集max
max = arr[i]
}
}
for(let j=0; j<=max; j++){ // 遍历哈希表,j是数组的数据
if(j in hashTable){ // 如果j在哈希里,就放到result,返回result
for(let i = 0; i<hashTable[j]; i++){
result.push(j)
}
}
}
return result
}
冒泡排序(bubbleSort)
冒泡排序是一种简单的排序算法,它重复地走访过要排序的数列,一次比较两个元素,如果它顺序错误就把它们交换过来
走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成
实现思路:
步骤1:比较相邻的元素,如果第一个比第二个大,就交换它们两个
步骤2:对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数
步骤3:针对所有的元素重复以上的步骤,除了最后一个
重复步骤1~3,直到排序完成
时间复杂度:O(n^2) 空间复杂度O(1)
代码实现
function bubbleSort(arr){ // 使用2层循环执行排序
for(let i = 0; i < arr.length - 1; i++){ // 内循环每执行一次,外层的i-1,表示前面的数据完成排序
for(let j = 0; j < arr.length - i - 1; j++){// 内层循环每次将最小数据移动到数组最左边
if(arr[j] > arr[j + 1]){ // 对比相邻的2个元素,如果前面的元素比后面的大,就交换位置
swap(arr, j, j+1);
}
}
}
return arr;
}
function swap(arr, i, j){ // 换位,每次对比完相邻的的元素,满足条件进行交换位置
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
选择排序(selectionSort)
选择排序的原理,以升序排序为例,就是从数组的开头开始,用第一条数据和其他数据进行比较
取其中最小的数据与第一个位置的数据交换,再用第二条数据对后面的数据进行比较......如此反复
时间复杂度:O(n^2) 空间复杂度O(1)
代码实现
function selectionSort(arr){ // 使用2层循环执行排序
for(let i = 0; i < arr.length - 1; i++){ // 遍历数组,先记录最小值为i
let index = i; // index记录当前循环最小值的位置
for(let j = i+1; j < arr.length; j++){ // 内层循环从i后面1位开始寻找最小的数据
if(arr[index] > arr[j]){ // 最小数比当前数小,把当前数记录为最小数
index = j;
}
}
swap(arr, i, index); // 交换位置
}
return arr;
}
function swap(arr, i, j){ // 换位
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
归并排序(mergeSort)
归并排序的原理是将数组不断的对半拆分,直到拆分为单个元素的数组
然后将单个元素的数组进行组合,就排好序了,因为单个元素默认是排好序的
归并排序的核心是组合数组,组合时进行排序
时间复杂度:O(nlog^2n) 空间复杂度O(1)
代码实现
let mergeSort = arr =>{
let k = arr.length // 得到数组的长度
if(k===1){return arr} // 长度为1,返回数组
let left = arr.slice(0, Math.floor(k/2)) // 拆分成左右两个数组,从数组的中间位置
let right = arr.slice(Math.floor(k/2))
return merge(mergeSort(left), mergeSort(right)) // 对拆分的数组继续拆分直到拆分成单个数组
}
let merge = (a, b) => { // 使用递归,合并单个元素的数组
if(a.length === 0) return b // a数组为空时返回数组b
if(b.length === 0) return a // b数组为空时返回数组a
return a[0] > b[0] ? // 对比a,b数组第一位的大小
[b[0]].concat(merge(a, b.slice(1))) : // 如果b[0]小,就用b[0]连接a和b的其它部分的递归
[a[0]].concat(merge(a.slice(1), b)) // 如果a[0]小,就用a[0]连接a的其它部分和b的递归
}
插入排序(insertSort)
插入排序的思路:将元素插入到已排序好的数组中
具体步骤
将第一个元素标记为已排序,遍历每个没有排序过的元素,提取元素
记录最后排过序的元素指数,将新提取的元素和已经排序过的元素进行对比
如果排过序的元素大于新提取的元素,将排序过的元素向右移一格;否则,插入提取的元素
时间复杂度:O(n^2) 空间复杂度O(1)
代码实现
function insertSort(arr) {
var value, // 当前未排序要进行比较的值
i, // 表示未排序部分的当前元素
j; // 表示已排序部分的当前元素
for (i=0; i < arr.length; i++) { // 遍历未排序部分,提取的元素
value = arr[i]; // 当前未排序位置的元素的值
for (j=i-1; j > -1 && arr[j] > value; j--) { // 当已排序部分的当前元素大于提取的元素
arr[j+1] = arr[j]; // 将当前元素向后移一位,再将前一位与提取的元素比较
}
arr[j+1] = value; // 插入提取的元素
}
return arr;
}