1.题目912. 排序数组
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入: nums = [5,2,3,1]
输出: [1,2,3,5]
示例 2:
输入: nums = [5,1,1,2,0,0]
输出: [0,0,1,1,2,5]
2.答题
1.冒泡排序
原理
- 从左到右边,依次比较左右两个元素的大小,如果左边比右边大,互换位置
- 一直比较到最后一个元素,这样一次循环下来,最大的一定在右边
- 第二次循环则只需要(遍历+判断替换)到最后一个的前一个,因为最后一个已经是确认最大的了
- 一直循环下去,直到左边已经没有可以循环比较的元素
代码实现
/**
* @param {number[]} nums
* @return {number[]}
*
*/
//通用交替方法
function swap(nums, i, j) {
;[nums[i], nums[j]] = [nums[j], nums[i]]
}
// 冒泡排序1 每次找最大值
//双循环,每一次循环 比较 j-1 与j的关系,是上一个与当前关系
const sortArray = nums => {
const n = nums.length
for (let i = n - 1; i > 0; i--) {//从尾巴开始,给j的总长度慢慢递减,
//这里其实跟“冒泡排序2” 类似,只是i 作为了下面j的终止条件j <= i,而不是开始条件,最终j还是从下标0到大开始遍历
for (let j = 1; j <= i; j++) { //总长度依次慢慢缩小为1,上一个j与当前j比较,
if (nums[j - 1] > nums[j]) {
swap(nums, j - 1, j)
}
}
}
return nums
}
// 冒泡排序2 每次找最小值
// 直接双循环,从左往右边比较 i和j的关系,是当前与下一个关系
var sortArray = function(nums) {
for(let i = 0; i < nums.length -1; i ++) { //这里 最后一个i 只需要到倒数第二就ok,倒数第一 j++就没意义,[5,1,1,2,4,0] ,比如这里知道i只到4即可
for(let j = i + 1; j < nums.length; j ++) { //j的起点随着i慢慢递增
if(nums[i] > nums[j]) {
let temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
}
}
}
return nums
};
优化1 标记是否替换过
在每一次小循环时,如果当前对比没有替换动作,则后面的冒泡都可以省略。
var sortArray = function(nums) {
for(let i = 0; i < nums.length -1; i ++) {
let isSorted = true // 是否排过序,默认是
for(let j = 0; j < nums.length - i -1; j ++) { //j的起点随着i慢慢递增
if(nums[i] > nums[j]) {
let temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
isSorted = false // 有替换 让下一个比较继续
}
}
if(isSorted) {
//终止这次小循环
break;
}
}
return nums
};
优化2 设定边界值
通过记录最后一次边际替换值,来调整下一次小循环的边界长度。
var sortArray = function(nums) {
let lastChangeIndex = 0
let sortBorderLen = nums.length -1
for(let i = 0; i < nums.length -1; i ++) {
let isSorted = true // 是否排过序,默认是
for(let j = 0; j < sortBorderLen; j ++) { //j的起点随着i慢慢递增
if(nums[i] > nums[j]) {
let temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
isSorted = false // 有替换 让下一个比较继续
lastChangeIndex = i //记录当前结束的边界
}
}
sortBorderLen = lastChangeIndex //改变下一次小循环的长度
if(isSorted) {
//终止这次小循环
break;
}
}
return nums
};
优化3 鸡尾酒排序
鸡尾酒排序 : 双向冒泡排序,因为鸡尾酒就是多种酒混搭,不按常规出牌。
主要逻辑: 像一个大钟摆,从左到右边便利,然后再从右往左边便利,一旦发现所有都不需要替换就立马停止。
var s = [1,2,3,4,5,8,6,7];
var cocktailSort = function(array) {
var count = 0;
var temp;
for(var i = 0; i < array.length/2; i++) {
var flag = false; //定义flag
for(var j = count; j < array.length - count -1; j++) {
if(array[j] > array[j+1]) {
temp = array[j];
array[j] = array[j+1];
array[j+1] = temp;
flag = true; //当发生交换时,改变flag的值为true
}
}
count++;
for(var k = array.length - 1 - count; k > count - 1; k--) {
if(array[k] < array[k-1]) {
temp = array[k];
array[k] = array[k-1];
array[k-1] = temp;
flag = true; //当发生交换时,改变flag的值true
}
}
console.log(array);
if(!flag)
break;
}
};
cocktailSort(s);
//=>[ 1, 2, 3, 4, 5, 6, 7, 8 ] //输出结果显示,算法会提前结束
2.选择排序
原理
思路
选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。(第一次每个元素都要遍历一遍比较)
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
代码实现
// 选择排序1
//1.从原序列中找到最小值,与数组第一个元素交换;
//2.除第一个元素外,从剩下未排序的序列中找到最小值,与数组第二个元素交换;
//3.共N-1趟,每趟都找到未排序的最小值,放到已排序的序列后面。
const sortArray = nums => {
const n = nums.length
for (let i = n - 1; i > 0; i--) {//从最后一个元素开始遍历,
let minIndex = 0 //刚开始拿出第一个元素5,索引0与其他所有元素比较,[5,4,3,6]
for (let j = 0; j <= i; j++) {
if (nums[j] < nums[minIndex]) minIndex = j //找到最小的那个值,把index替换到0的位置
//第一次拿5跟5比较,
//第二次4跟5比较 替换位置,minIndex = 1
//第三次3跟4比较 替换位置,minIndex = 2
//第三次6跟4比较 minIndex = 2 不变
}
swap(nums, i, maxIndex)//这里交换索引位置 0 和 2的位置
}
return nums
}
3.插入排序
- 插入排序的核心是
插入,也是如同选择排序,将一个数组分为有序数组和无序数组。 - 不同的是,他是将无序数组的第一位插入到有序数组的合适位置,并将该位置之后的有序数组统一往后移动一位,继续遍历无序数组直到无序数组长度为0。
- 核心算法是找到有序数组中合适位置
代码实现
// 插入排序
// 通过从左起(左边一直是有序的),依次在右边乱的数据拿出一个,插入到左边准确的位置。
// -InsertionSort 和打扑克牌时,从牌桌上逐一拿起扑克牌,在手上排序的进程相同。
// Input: {4, 3, 8, 5, 2, 6, 1, 7}。
// 1.首先拿起第一张牌, 手上有 {4}。
// 2.拿起第二张牌 3, 把 3insert 到手上的牌 {4}, 得到 {3 ,4}。
// 3.拿起第三张牌 8, 把 8 insert 到手上的牌 {3,4 }, 得到 {3 ,4,8}。
const sortArray = nums => {
for (let i = 1; i < nums.length; i++) {//i不断递增,
let j = i
while (j > 0 && nums[j - 1] > nums[j] ) {//j与上一个j 比较,长度判断从j=1,不断趋近于j=nums.length
// 由于左边已经是有序的,从小到大,所以左边最后一个nums[j - 1]是左边最大值,只需要判断nums[j - 1] 小于 nums[j] 才需要进行进一步判断替换
swap(nums, j, j - 1)//这里开始替换
j--
}
}
return nums
}
4.归并排序
原理
少做事情的思想
简单版本
- ,把数组一分为二,各自进行冒泡排序,让把两个排好的数组进行合并。
- 合并的时候,由于左边依次从左边第一个,与右边的第一个比较,较小的拿出放到结果列表里。
- 如上面的 2和6 ,取2。
- 继续比较:4和6 ,取4。
- 继续比较:19和6 ,取6。
- 继续比较:19和10 ,取10。
- 继续比较:19和13 ,取13。
- 继续比较:19和20 ,取19。
- 继续比较:40和20 ,取20。
- 继续比较:最后一个40。
这里关键在于:由于左右两边都是排好序,最小的元素肯定是两个数组中其中一个。所以永远按两个元素的首元素比较取出,则永远都能拿到最小值。
升级版本
通过递归的方式,把数组继续拆分到单个不能再拆分。然后再回溯合并。
此过程叫做分治法(Divide and Conquer),也叫二路归并
- 自上而下的递归
- 自下而上的迭代 使用网上的流行图:
依次拆分到最小元素如:(3,44) (38,5),然后开始 (小排序+小排序) 合并成一个新的有序数组
代码实现
// 归并排序- 普通
function merge(left,right){
var temp=[];
//这里要合并两个 分割的数组,如[4,5,7,8]和[1,2,3,6],最终要得到[1,2,3,4,5,6,7,8]
//通过比较两个数组的头元素,如4和1,1比较小,1加入temp,移动,不断插入数据到临时数组 temp上面
while(left.length&&right.length){
if(left[0]<right[0]){
temp.push(left.shift());
}else{
temp.push(right.shift());
}
}
// 由于left,right本身是有序的, 直接连接剩下的长的部分
return temp.concat(left,right);
}
//用于拆分
function arrSplitAndMerge(data){
if(data.length<=1){
return data;
}
var mid=Math.floor(data.length/2);
var left=data.slice(0,mid);
var right=data.slice(mid);
return merge(arrSplitAndMerge(left),arrSplitAndMerge(right));//递归拆分,拆分完进行 回溯合并
}
const sortArray = (nums) => {
return arrSplitAndMerge(nums);
}
// 归并排序 高效版-优化合并数组逻辑
const sortArray = (nums, left = 0, right = nums.length - 1) => {
if (left >= right) return nums // 当left = 0 直接返回单个数字
const mid = (left + right) >> 1 //总长度除于2
sortArray(nums, left, mid)
sortArray(nums, mid + 1, right)
let i = left
let j = mid + 1
let index = 0
const arr = new Array(right - left + 1)
//console.log( "nums",nums)
//这里要合并两个 分割的数组,如[4,5,7,8]和[1,2,3,6],最终要得到[1,2,3,4,5,6,7,8]
//通过比较两个数组的头元素,移动i和j的游标,不断插入数据到临时数组 arr上面
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) arr[index++] = nums[i++]
else arr[index++] = nums[j++]
}
//把剩下的数据都插到 arr上面
while (i <= mid) arr[index++] = nums[i++]
while (j <= right) arr[index++] = nums[j++]
for (let k = 0; k < arr.length; k++) {//这里把 合并后的数组返回。由于是共用方法,需要区分合并的起点left位置
nums[left + k] = arr[k]//这里不创建其他临时存储空间,直接把更新后的排序内容赋值到原来的索引上面
//比如[8, 5, 10, 3, 2, 18, 17, 9],中拆分到 10, 3的时候,排序后是 3,10 赋值到 索引2,3的位置,所以循环从left+k开始
}
//console.log("left",left,"arr",arr,"nums",nums)
return nums
}
//let dat=[8, 5, 10, 3, 2, 18, 17, 9];
//console.log( sortArray(dat))
5.快速排序
原理
- 在一堆无序的数字里(最大为100)。挑选一个,比如51,这个51就是叫做
枢值。 - 把所有小于51的分为A组,所有大于等于51放B组。
- 依次继续再A组里面随机选一个值比如30做
枢值(由于A组都是小于51,所有拿到的值一定在0-50之间) - 则A被分解成:C组0-29,D组30到50。
- 同理 B 取出的是 70,则分成:E组51-69,F组70到100。
- 直到不能再细分组为止。
由于每次分组操作对下一次都是有意义的,没有做过多的无用功。
实现逻辑
- 双边循环法
- 使用left 和right代表左右两端指针,每次与最左边的枢值pivot比较。(枢值也可以是随机不一定要最左边)
- right先开始,当大于等于pivot往左边移动,否则停止,轮到left移动。
- 当left小于等于pivot往右边移动,否则停止,到此一次循环结束。替换当前left和right对应的值。
- 一直right和left移动,直到left和right重合,把left对应的值,和刚开始枢值pivot 替换。
- 到此一次二分结束。
- 单边循环法
- 以最左边元素做枢值pivot。(枢值也可以是随机不一定要最左边)
- 设置一个mark指针 代表小雨基准元素的区域边界,位置也是从最左边元素开始往右边遍历。
- 当遇到下一个元素大于枢值,就继续移动遍历,不移动mark。
- 当遇到下一个元素小于枢值
- 1.把mark往右边移动一位(留出1个空位的给小值放)
- 2.把mark对应的值与下一个元素交换位置。
- 然后一直这样遍历到数组结束,最后把枢值和mark值替换即可。
核心思想:mark是一个动态的中间值,遇到大的值丢mark的右边(默认就在右边),遇到小的值mark左边留个空位,丢到mark的左边。
代码实现
快速排序
//双指针版本
function quickSort(arr, begin, end) {
//递归出口
if(begin >= end)
return;
var l = begin; // 左指针
var r = end; //右指针
var temp = arr[begin]; //基准数,这里取数组第一个数
//左右指针相遇的时候退出扫描循环
while(l < r) {
//右指针从右向左扫描,碰到第一个小于基准数的时候停住
while(l < r && arr[r] >= temp)
r --;
//左指针从左向右扫描,碰到第一个大于基准数的时候停住
while(l < r && arr[l] <= temp)
l ++;
//交换左右指针所停位置的数
[arr[l], arr[r]] = [arr[r], arr[l]];
}
//最后交换基准数与指针相遇位置的数
[arr[begin], arr[l]] = [arr[l], arr[begin]];
//递归处理左右数组
quickSort(arr, begin, l - 1);
quickSort(arr, l + 1, end);
}
var arr = [2,3,4,1,5,6]
quickSort(arr, 0, 5);
console.log(arr)
//单指针 版本
function quick_sort(list, start, end) {
if (start < end) {
var pivotpos = partition(list, start, end); //找出快排的基数
quick_sort(list, start, pivotpos - 1); //将左边的快排一次
quick_sort(list, pivotpos + 1, end); //将右边的快排一次
}
}
//将一个序列调整成以基数为分割的两个区域,一边全都不小于基数,一边全都不大于基数
function partition(list, start, end) {
var pivotpos = start;
var pivot = list[start];
var tmp;
for(var i = start + 1; i <= end; i ++) {
if (list[i] < pivot) {
tmp = list[i];
pivotpos += 1;
list[i] = list[pivotpos];
list[pivotpos] = tmp;
}
}
tmp = list[start];
list[start] = list[pivotpos];
list[pivotpos] = tmp;
return pivotpos;
}
//测试代码
var list = [8,2,4,65,2,4,7,1,9,0,2,34,12];
quick_sort(list, 0, list.length);
// 双指针 往里走版 升级版
const sortArray = (nums, left = 0, right = nums.length - 1) => {
if (left >= right) return nums
let i = left
let j = right - 1
while (i <= j) {
if (nums[i] > nums[right]) {
;[nums[i], nums[j]] = [nums[j], nums[i]]
j--
} else {
i++
}
}
j++
;[nums[j], nums[right]] = [nums[right], nums[j]]
sortArray(nums, left, j - 1)
sortArray(nums, j + 1, right)
return nums
}
// 快速排序 易懂版+效率也高 取中间值,拼接
function sortArray(arr){
//如果数组只有一个数,就直接返回;
if(arr.length<1){
return arr;
}
//找到中间的那个数的索引值;如果是浮点数,就向下取整
var centerIndex = Math.floor(arr.length/2);
//根据这个中间的数的索引值,找到这个数的值;
var centerNum = arr.splice(centerIndex,1);
//存放左边的数
var arrLeft = [];
//存放右边的数
var arrRight = [];
for(i=0;i<arr.length;i++){
if(arr[i]<=centerNum){
arrLeft.push(arr[i])
}else if(arr[i]>centerNum){
arrRight.push(arr[i])
}
}
return sortArray(arrLeft).concat(centerNum,sortArray(arrRight));
};
6.桶排序
原理
- 先分桶,划分合适数量的桶。
- 将所有待排序数据放入到对应的桶中。
- 使用合理的算法对每个非空桶进行子排序。
- 按顺序将每个桶中数据进行合并。
代码实现
// 桶排序
const sortArray = nums => {
const each = 100 // 每个桶的范围
let min = Number.MAX_SAFE_INTEGER
let max = Number.MIN_SAFE_INTEGER
for (const num of nums) {
num < min && (min = num)
num > max && (max = num)
}
const count = max - min + 1
const bucketCount = count % each === 0 ? count / each : Math.floor(count / each) + 1
const buckets = new Array(bucketCount)
for (const num of nums) {
const cur = Math.floor((num - min) / each)
if (!buckets[cur]) buckets[cur] = []
buckets[cur].push(num)
}
let index = 0
for (const bucket of buckets) {
if (!bucket) continue
// 对每个桶内的元素排序,直接使用 sort
bucket.sort((pre, next) => pre - next)
for (const num of bucket) {
nums[index++] = num
}
}
return nums
}
7.堆排序
原理
(二叉)堆:是一个数组,可以看成是一个完全二叉树。
- 堆是一个完全二叉树。 完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
- 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。 也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。
最大堆:除了根节点以外的所有结点,其结点的值小于等于其父节点,A[parent(i)] >= A[i]。因此堆中最大元素存放于根结点中。
最小堆:除了根节点以外的所有结点,其结点的值大于等于其父节点,A[parent(i)] <= A[i]。因此堆中最小元素存放于根结点中。
完全二叉树有个特性:左边子节点位置 = 当前父节点的两倍 + 1,右边子节点位置 = 当前父节点的两倍 + 2
简单来说: 堆其实可以用一个数组表示,给定一个节点的下标 i (i从1开始) ,那么它的父节点一定为 A[i/2] ,左子节点为 A[2i] ,右子节点为 A[2i+1]
- i 结点的父结点 par = floor((i-1)/2) 「向下取整」
- i 结点的左子结点 2 * i + 1
- i 结点的右子结点 2 * i + 2
实现逻辑
利用删除堆顶会导致堆重现排列的特性,并且删除的堆顶就是最大值,刚好插在末尾节点上。 依次删除所有的堆顶。结束时候就是有序的。是一个下沉的过程。
- 先把无序的数组构建成二叉堆。利用最大堆解决从小到大排序。利用最小堆解决从大到小排序。
- 循环删除堆顶,替换到二叉堆的末尾,并且调整堆产生新的堆。
代码实现
// 堆排序 详细注释版本
const sortArray = array => {///////‘
// 1. 初始化大顶堆,从第一个非叶子结点开始,把无序的数组变成最大堆
for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
heapify(array, i, array.length);//下沉
}
// 2. 排序,每一次 for 循环找出一个当前最大值,数组长度减一
// 循环删除堆顶的元素,移到集合的末端,在调整堆变成 新的堆顶
for (let i = Math.floor(array.length - 1); i > 0; i--) {
// 根节点与最后一个节点交换 相当于顶堆删除动作
swap(array, 0, i);
// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
heapify(array, 0, i);
}
return array;
};
// 交换两个节点
const swap = (array, i, j) => {
let temp = array[i];
array[i] = array[j];
array[j] = temp;
};
// 下沉方法
// 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
// 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
// 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
// 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
// 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
const heapify = (array, i, length) => {
let temp = array[i]; // 当前父节点
// j < length 的目的是对结点 i 以下的结点全部做顺序调整
for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
if (j + 1 < length && array[j] < array[j + 1]) {
j++; // 找到两个孩子中较大的一个,再与父节点比较
}
if (temp < array[j]) {
swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
i = j; // 交换后,temp 的下标变为 j
} else {
break;
}
}
};
//const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
//console.log('原始array:', array);
//const newArr = sortArray(array);
//console.log('newArr:', newArr);
// 堆排序 方案2
const sortArray = nums => {
heapify(nums)
for (let i = nums.length - 1; i > 0; i--) {
swap(nums, i, 0)
rebuildHeap(nums, 0, i - 1)
}
// 数组转成最大堆
function heapify(nums) {
for (let i = 1; i < nums.length; i++) {
let parent = (i - 1) >> 1
let child = i
while (child > 0 && nums[child] > nums[parent]) {
swap(nums, parent, child)
child = parent
parent = (parent - 1) >> 1
}
}
}
function rebuildHeap(nums, parent, last) {
const left = 2 * parent + 1
const right = 2 * parent + 2
let maxIndex = left
if (right <= last && nums[right] > nums[left]) {
maxIndex = right
}
if (maxIndex <= last && nums[maxIndex] > nums[parent]) {
swap(nums, maxIndex, parent)
rebuildHeap(nums, maxIndex, last)
}
}
return nums
}
8.计数排序
原理
计数排序的核心在于将数据值转化为数组的下标、然后利用数组下标天然有序的特征。
缺点:如果是有很大数字10000000,会占用很多空间
算法步骤
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C 的第i项
- 对所有的计数累加 (从C 中的第一个元素开始,每一项和前一项相加)
1.创建最大数组
2.把每个值放到数组里 对应的index里,重复就累加
3.最后遍历数组依次取出
代码实现
计数排序
// 计数排序
const sortArray = nums => {
let min = Number.MAX_SAFE_INTEGER
let max = Number.MIN_SAFE_INTEGER
for (const num of nums) {
num < min && (min = num)
num > max && (max = num)
}
const count = new Array(max - min + 1).fill(0)//这里max - min 来优化总长度,当要访问的时候 使用num - min做实际访问的下标。
for (const num of nums) {
count[num - min]++
}
let index = 0
for (let i = 0; i < count.length; i++) {
while (count[i] > 0) {
nums[index++] = i + min
count[i]--
}
}
return nums
}
9.希尔排序
希尔排序是插入排序的改进版本,也成为缩小增量排序(Diminishing Increment Sort)。
原理:
- 将待排序的数组元素按下标的一定增量分组,将其分成多个子序列
- 对各子序列进行插入排序
- 依次缩减增量重复执行排序操作
- 直至增量缩小为
1时进行最后一次插入排序
- 希尔排序增量(希尔增量)范围:
[1, 待排序数组长度) - 取值一般从 待排序数组长度一半 开始
- 后续每次减半,直至增量为
1
以长度为9的待排序数组为例:
- 第一个增量:
9 / 2 = 4 - 第二个增量:
4 / 2 = 2 - 第三个增量:
2 / 2 = 1
代码实现
const sortArray = nums => {
const n = nums.length
let gap = n >> 1
while (gap > 0) {
for (let i = 0; i < gap; i++) {
for (let j = i + gap; j < n; j += gap) {
let temp = j
while (temp > i && nums[temp] < nums[temp - gap]) {
swap(nums, temp, temp - gap)
temp -= gap
}
}
}
gap >>= 1
}
return nums
}
10 基数排列
原理
主要思想:将整数按位数切割成不同的数字,然后按每个位数分别比较从而得到有序的序列。
步骤:
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
- 创建 0- 9 的10位数组。
- 依次从把数字的第一位匹配 数组位置,并放入
- 把数据的数字,从左到有依次拿出,成为新数组
- 然后重复3的步骤,只是规则改成从第二位开始
- 直到最前面一位也放完和取出。得到最终的数组
缺点:如果数字位数很大,效率很低,适合位数少的数组。
代码实现
//LSD Radix Sort
var counter = [];
function radixSort(arr, maxDigit) {
var mod = 10;
var dev = 1;
for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
for(var j = 0; j < arr.length; j++) {
var bucket = parseInt((arr[j] % mod) / dev);
if(counter[bucket]==null) {
counter[bucket] = [];
}
counter[bucket].push(arr[j]);
}
var pos = 0;
for(var j = 0; j < counter.length; j++) {
var value = null;
if(counter[j]!=null) {
while ((value = counter[j].shift()) != null) {
arr[pos++] = value;
}
}
}
}
return arr;
}
刷题
剑指 Offer 45. 把数组排成最小的数
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
示例 1:
输入: [10,2]
输出: "102"
示例 2:
输入: [3,30,34,5,9]
输出: "3033459"
答题
/**
* @param {number[]} nums
* @return {string}
*/
var minNumber = function(nums) {
return nums.sort((a,b) => `${b}${a}` - `${a}${b}`).join("")
};
3. 对比
1.类别
比较类:
- 交换类排序:快速排序、冒泡排序
- 插入类排序:简单插入排序、希尔排序
- 选择类排序:简单选择排序、堆排序
- 归并排序:二路归并排序、多路归并排序
非比较类:
- 计数排序
- 基数排序
- 桶排序
对数组元素的要求:
- 计数排序、桶排序: 非负整数
- 基数排序:整数
2.稳定性
稳定的排序算法:
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 桶排序
- 基数排序
不稳定的排序算法:
- 快速排序
- 希尔排序
- 堆排序
3.时间复杂度
平方阶 (O(n^2)) 排序 各类简单排序:
- 冒泡排序
- 直接选择
- 直接插入
线性对数阶 (O(nlog2n)) 排序
- 快速排序
- 堆排序
- 归并排序
O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数
- 希尔排序
线性阶 (O(n)) 排序
- 基数排序
- 桶、箱排序
二、综合对比表
| 排序算法 | 平均 | 最差 | 最好 | 空间复杂度 | 稳定性 | 是否原地 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O( n^2 ) | O( n^2 ) | O( n^2 ) | O(1) | 稳定 | 原地 |
| 选择排序 | O( n^2 ) | O( n^2 ) | O( n^2 ) | O(1) | 稳定 | 原地 |
| 插入排序 | O( n^2 ) | O( n^2 ) | O( n ) | O(1) | 稳定 | 原地 |
| 希尔排序 | O(nlog2n) | O( n^2 ) | O( nlog(n) ) | O(1) | 不稳定 | 原地 |
| 归并排序 | O( nlog(n) | O( nlog(n) | O( nlog(n) | O(n) | 稳定 | 非原地 |
| 快速排序 | O( nlog(n) ) | O( n^2 ) | O( nlog(n) ) | O(1) | 不稳定 | 原地 |
| 堆排序 | O( nlog(n) | O( nlog(n) | O( nlog(n) | O(1) | 不稳定 | 原地 |
| 计数排序 | O( n+k ) | O( n+k ) | O( n+k ) | O(n + k) | 稳定 | 非原地 |
| 桶排序 | O( n+k ) | O( n^2 ) | O( n+k ) | O(n + k) | 稳定 | 非原地 |
| 基数排序 | O( k * n ) | O( k * n ) | O( k * n ) | O(n + k) | 稳定 | 非原地 |