题意描述:
给定一个整数数组 nums,将该数组升序排列。
数据范围 1 <= A.length <= 10000 ,-50000 <= A[i] <= 50000
示例:
输入:[5,2,3,1]
输出:[1,2,3,5]
输入:[5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
解题思路:
Alice: 排序的N种写法 ??
Bob: 看来是的。
Alice: 桶排序 ?
Bob:桶就像是 Python 中的字典,或者 Java 中的 map, 记录每个元素出现的次数,然后按照 key 的大小遍历 字典,得到的就是从小到大有序的数组。只不过我们是直接用数组实现的,开了一个很大的数组记录每个元素出现的次数,如果是字典,内存消耗会小很多,但是字典好像没有办法保证按照 key 的大小遍历。
Alice: 不错不错。选择排序 ?
Bob: 选择排序就是 每次找出 第 k 小的元素放到 下标是 k-1 的位置上,外层循环可以是 n-1 次,因为第 n-1 小的元素归位后,整个数组就已经是有序的了。
Alice: 插入排序 ?
Bob: 插入排序就是不断往左侧有序的数组中插入元素,并且维持左侧的数组始终有序。
Alice: 插入排序是怎么实现插入的呢? 怎么找到要插入的位置呢?
Bob: 插入有两种实现方式,一种是不断交换相邻元素的值,直到待插入的元素被交换到要插入的位置,第二种就是循环找到要插入的位置,然后交换一次。
Alice: 插入排序就是将 第 1,2,3…个位置的元素依次插入到左侧的数组中去,这就是外层循环所做的事情。
Bob: 是的,然后还有快速排序 ?
Alice: 快速排序就是 划分数组, 首先选定一个 主元,第一次划分的结果是,这个主元归位到正确的位置, 左侧的元素都比这个主元小,右侧的元素都比这个主元大。然后对左侧的数组再划分,对右侧的数组也再划分,递归下去,一直到待划分的数组长度是1的时候就是递归终止的时候。不错,怎么确定 主元呢 ? 可以随机选取,不过一般都是指定待排序数组最左侧或者最右侧的元素。划分的具体实现,就是在数组两侧找到两个需要交换的元素,然后交换位置。交换的实现方式也有两种,一种是直接交换,一种是通过 “覆盖” 的方法分两次 将待交换的元素防止到正确的位置上。
Bob: 那我来讲一个归并排序吧,归并排序的本质就是利用递归不断的合并两个有序数组。合并两个有序数组就是一个双指针的问题,然后归并排序就是在先划分整个数组为很多小数组,然后在合并有序数组。
Alice: 我讲一下冒泡排序吧,冒泡排序就是通过比较和交换相邻元素实现每趟将一个最大值或者最小值冒泡到数组末尾或者头部的算法,它的时间复杂度是O(n*2)。内外两层循环都可以做一些优化的操作。
Bob:还有希尔排序,希尔排序是插入排序的优化版本,插入排序是直接不断的将元素插入到一个有序数组中去。希尔排序是先将数组划分为若干个小数组,在小数组内部不断使用插入排序,这样整个大数组也是逐渐有序的,最后再对整个数组使用插入排序。希尔排序虽然写起来是三重循环,但是它的平均时间复杂度是 O(n log n)。
Alice: 堆排序,堆排序要在一个数组中建立完全二叉树,然后通过这个二叉树来维护大顶堆。这个二叉树有个特点,leftIndex = rootIndex * 2 + 1, rightIndex = rootIndex * 2 + 2 == leftIndex + 1
然后排序的过程就是先建立一个大顶堆,大顶堆的定义是对二叉树中的每个节点,父节点的值要大于或者等于两个子节点的值。建完之后,将堆顶的元素交换到数组末尾,然后在[0, length-k] 的范再维护大顶堆,虽然是在数组中建立完全二叉树,但是却没有使用递归。堆排序还是很不错的。
Bob: 终于总结完了哈。
Alice: 还早着呢,还有什么随机排序,基数排序,排序的稳定性分析呢 !
Bob: 🙄🙄
代码:
Python 方法一: 库函数
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
nums.sort()
return nums
JavaScript 方法一: 库函数 + 自定义比较函数
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
return nums.sort(sortNumber); // 注意sort方法默认按照字符串排序 汗
};
var sortNumber = function(a, b){
return a - b;
}
Python 方法二: 桶排序
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
bound = 50000 * 2 + 10
buckets = [0 for x in range(bound)] # 设定桶的大小
for x in nums:
buckets[x + 50000] += 1 # 数据存储到桶中
ret = []
for x in range(len(buckets)): # 按顺序读取就是排序结果
if buckets[x]:
ret.extend([x - 50000] * buckets[x])
return ret
JavaScript 方法二: 桶排序, 数组太大,javascript heap out of memory
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
// 初始化桶
let bound = 50000 * 2 + 10;
let buckets = new Array(bound);
buckets.fill(0);
// 存储进桶中
for(let num of nums){
buckets[num + 50000] ++;
}
//按顺序读取
let ret = [];
for(let i=0; i<buckets.length; ++i){
while(buckets[i] > 0){
ret.push(i - 50000);
}
}
return ret;
};
JavaScript 方法三: 选择排序, 每次找出第 k 小的元素放到合适的位置。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
for(let i=0; i<nums.length; ++i){
let minIndex = i;
// 每次找出第 k 小的元素放到合适的位置
for(let j=i+1; j<nums.length; ++j){
if(nums[j] < nums[minIndex]){
minIndex = j;
}
}
swap(nums, minIndex, i);
}
return nums
};
var swap = function(nums, indexa, indexb){
let tmp = nums[indexa];
nums[indexa] = nums[indexb];
nums[indexb] = tmp;
return ;
}
JavaScript 方法四: 冒泡排序
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
for(let i=0; i<nums.length-1; ++i){ // 冒泡排序仅仅需要 n-1 趟
for(let j=0; j<nums.length-1; ++j){ // 每趟冒泡一个最大值到数组末尾
if(nums[j] > nums[j+1]){
swap(nums, j, j+1);
}
}
}
return nums;
};
var swap = function(nums, indexa, indexb){
let tmp = nums[indexa];
nums[indexa] = nums[indexb];
nums[indexb] = tmp;
return ;
}
JavaScript 方法四 冒泡排序,优化循环,及时跳出循环。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
for(let i=0; i<nums.length-1; ++i){
let order = true; // 记录是否已经有序
for(let j=0; j<nums.length-1-i; ++j){ // 只有前面的nums.length-1-i 个元素是无序的
if(nums[j] > nums[j+1]){
swap(nums, j, j+1);
order = false;
}
}
if(order == true){
break;
}
}
return nums;
};
var swap = function(nums, indexa, indexb){
let tmp = nums[indexa];
nums[indexa] = nums[indexb];
nums[indexb] = tmp;
return;
}
JavaScript 方法五: 插入排序,通过交换相邻元素实现插入
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
for(let i=1; i<nums.length; ++i){ // 每次插入一个元素到 前 i 个元素中
for(let j=i; j>=1; --j){ // 前 i 个元素都是有序的
if(nums[j] < nums[j-1]){ // 通过连续交换相邻元素插入到合适位置
swap(nums, j, j-1);
}else{
break;
}
}
}
return nums;
};
var swap = function(nums, indexa, indexb){
let tmp = nums[indexa];
nums[indexa] = nums[indexb];
nums[indexb] = tmp;
return;
}
JavaScript 方法五, 插入排序,通过移动数组,实现插入。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
for(let i=1; i<nums.length; ++i){
let tmp = nums[i];
let index = 0;
for(let j=i-1; j>=0; --j){
if(nums[j] > tmp){
nums[j+1] = nums[j]; // 移动数组
}else{
index = j + 1; // 最后要插入的位置
break;
}
}
nums[index] = tmp;
}
return nums;
};
JavaScript 方法六,希尔排序,其中使用的插入排序方式为 交换相邻组内元素。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
//shellsort
for(let gap=parseInt(nums.length/2); gap>=1; gap=parseInt(gap/2)){ // 使用gap实现分组
for(let i=gap; i<nums.length; ++i){ // 跨组插入排序,和分组别插入排序效果一样
for(let j=i; j>=gap; j-=gap){ // 在每个组内插入排序
if(nums[j] < nums[j-gap]){
swap(nums, j, j-gap);
}else{
break;
}
}
}
}
return nums;
};
var swap = function(num, indexa, indexb){
let tmp = num[indexa];
num[indexa] = num[indexb];
num[indexb] = tmp;
return;
}
JavaScript 方法七,归并排序,递归 + 合并两个有序数组
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
let tmp = new Array(nums.length); // 从始至终使用的O(n)的数组
mergeSort(nums, 0, nums.length-1, tmp); // 归并排序
return nums;
};
var mergeSort = function(nums, left, right, tmp){
if(left >= right){
return;
}
let middle = parseInt((left + right) / 2);
mergeSort(nums, left, middle, tmp);
mergeSort(nums, middle + 1, right, tmp);
merge(nums, left, right, middle, tmp);
// 合并从 left 到 middle 以及 从 middle + 1 到 right 的两个有序数组
}
var merge = function(nums, left, right, middle, tmp){
// 合并 nums 中[left, middle], [middle+1, right] 到 tmp[1, K]
let i = left;
let j = middle + 1;
let t = 0;
while(i <= middle && j <= right){
if(nums[i] < nums[j]){
tmp[t++] = nums[i++];
}else{
tmp[t++] = nums[j++];
}
}
while(i<=middle){
tmp[t++] = nums[i++];
}
while(j<=right){
tmp[t++] = nums[j++];
}
// 将合并后的有序数组放置到 nums 中指定的位置
t = 0;
let start = left;
while(start <= right){
nums[start++] = tmp[t++];
}
return;
}
JavaScript 方法八, 快速排序,通过“反复横跳”实现 partition
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
quickSort(nums, 0, nums.length-1);
return nums;
};
var quickSort = function(nums, left, right){
if(left >= right){
return;
}
let l = left;
let r = right;
let anchor = nums[l];
while(l < r){
// 从后往前找到一个 小于anchor 的元素 放到anchor 左侧
while(l < r && nums[r] >= anchor){
r--;
}
nums[l] = nums[r];
// 从前往后找到一个 大于等于 anchor 的元素 放到 anchor 右侧
while(l < r && nums[l] < anchor){
l ++;
}
nums[r] = nums[l];
}
let anchorIndex = l;
nums[anchorIndex] = anchor;
quickSort(nums, left, anchorIndex-1);
quickSort(nums, anchorIndex+1, right);
}
JavaScript 方法九 堆排序,通过在数组中建立完全二叉树维护大顶堆。
/**
* @param {number[]} nums
* @return {number[]}
*/
var sortArray = function(nums) {
buildHeap(nums);
// 建立大顶堆
for(let i=nums.length-1; i>0; --i){
swap(nums, 0, i);
adjustHeap(nums, 0, i); // 调整新的大顶堆
}
return nums;
};
var buildHeap = function(nums){
for(let i=parseInt(nums.length/2)-1; i>=0; --i){
// 依次调整每科子树
adjustHeap(nums, i, nums.length);
}
return;
}
var adjustHeap = function(nums, rootIndex, length){
let tmp = nums[rootIndex]
for(let k=rootIndex*2+1; k<length; k=k*2+1){ // k索引到rootIndex所有的左子树
if(k+1 < length && nums[k+1] > nums[k]){ // 找到左右子树中较大的那一个
k++;
}
if(nums[k] > tmp){
nums[rootIndex] = nums[k]; // 维护大顶堆
rootIndex = k;
}else{
break;
}
}
nums[rootIndex] = tmp;
return;
}
var swap = function(nums, indexa, indexb){
let tmp = nums[indexa];
nums[indexa] = nums[indexb];
nums[indexb] = tmp;
return ;
}
易错点:
- 一些测试用例
[5,2,3,1]
[5,1,1,2,0,0]
[1]
[50000,-50000]
- 答案:
[1,2,3,5]
[0,0,1,1,2,5]
[1]
[-50000,50000]
总结: