O(n^2)=======================================
冒泡排序【稳定】⭐
冒泡排序是最基本的排序算法,排序思路如下:
1.一边比较一边向后两两交换,将最大值冒泡到最后一位。
2.循环该过程n-1次(n为数组长度),数组此时为升序排列。
1.2 动图演示
1.3 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
//冒泡排序
int n=nums.size();
for(int i=0;i<n-1;i++)
{
for(int j=1;j<n-i;j++)
{
if(nums[j]<nums[j-1])
{
int temp=nums[j];
nums[j]=nums[j-1];
nums[j-1]=temp;
}
}
}
return nums;
}
};
插入排序【稳定】⭐
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.1 算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
假设一个数组,在其内部,数已经按照升序排列,此时有一个新的数a要加入数组,那么数组内大于a的数字需不断地向后腾出位置,直到a找到自己的位置,就可以将a插入该位置,此时原数组仍保持升序排列。
同理,插入排序就是将已排序部分当成一个小数组,未排序部分将一个一个插入到小数组当中,循环插入,直至排序完成。
3.2 动图演示
3.2 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
//插入排序
int n=nums.size();
for(int i=1;i<n;i++)
{
int flag=nums[i];
int j=i-1;
while(j>=0&&flag<nums[j])
{
nums[j+1]=nums[j];
j--;
}
nums[j+1]=flag;
}
return nums;
}
};
3.4 算法分析
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
选择排序⭐
思想如下:
1.找到数组未排序部分的最小值交换至数组未排序部分首位。
2.与冒泡排序相同,循环该过程n-1次(n为数组长度),数组此时为升序排列。
2.2 动图演示
2.3 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
//选择排序
int n=nums.size();
for(int i=0;i<n-1;i++)
{
int min=i;
for(int j=i+1;j<n;j++)
{
if(nums[min]>nums[j])
min=j;
}
if(min!=i)
swap(nums[i],nums[min]);
}
return nums;
}
};
2.4 算法分析
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序,即高级插入排序,是对插入排序的优化,思路如下:
1.将一个长数组按照相同的间隔h分为多个小数组,每个小数组分别进行插入排序。
2.将间隔h缩小,并继续排序,直至间隔为1。
可以证明出当间隔h=3*h+1时,希尔排序平均时间复杂度最优,推理过程此处省略,有兴趣的读者可自行查询。
4.2 动图演示
4.3 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
//希尔排序(高级插入排序)
int n=nums.size();
int h=1;
while(h<n/3)
h=3*h+1;
while(h>0)
{
for(int i=h;i<n;i++)
{
int j=i-h;
int flag=nums[i];
while(j>=0&&flag<nums[j])
{
nums[j+h]=nums[j];
j=j-h;
}
nums[j+h]=flag;
}
h=h/3;
}
return nums;
}
};
4.4 算法分析
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
O(nlogn)=======================================
快速排序⭐⭐
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
6.1 算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
6.2 动图演示
6.3 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
quickSort(nums,0,nums.size()-1);
return nums;
}
private:
void quickSort(vector<int>& nums,int left,int right){
if(left<right){
int standard=getStandard(nums,left,right);
quickSort(nums,left,standard-1);
quickSort(nums,standard+1,right);
}
}
int getStandard(vector<int>& nums,int left,int right){
int key=nums[left];
while(left<right){
while(left<right && nums[right]>=key){
--right;
}
nums[left]=nums[right];
while(left<right && nums[left]<=key){
++left;
}
nums[right]=nums[left];
}
nums[left]=key;
return left;
}
};
快排存在的问题,如何优化:
3 种快排基准选择方法: 随机(rand函数)、固定(队首、队尾)、三数取中(队首、队中和队尾的中间数)
4种优化方式:
- 优化1:当待排序序列的长度分割到一定大小后,使用插入排序
- 优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等 元素分割
- 优化3:优化递归操作
- 优化4:使用并行或多线程处理子序列
归并排序【稳定】⭐⭐
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
5.1 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
5.2 动图演示
5.3 代码实现
class Solution {
public:
vector<int> sortArray(vector<int>& nums) {
//在排序前,建一个临时数组,避免递归频繁开辟空间
vector<int>temp(nums.size());
merge_sort(nums,0,nums.size()-1,temp);
return nums;
}
private:
void merge_sort(vector<int> &nums,int left,int right,vector<int>temp){
if(left<right){
int mid = (left+right)/2;
merge_sort(nums,left,mid,temp);//左边归并排序,使得左子序列有序
merge_sort(nums,mid+1,right,temp);//右边归并排序,使得右子序列有序
merge(nums,left,mid,right,temp);//将两个有序子数组合并操作
}
}
void merge(vector<int> &nums,int left,int mid,int right,vector<int> temp){
int i = left,j = mid+1;//左右序列指针
int t = 0;//临时数组指针
while (i<=mid && j<=right){
if(nums[i]<=nums[j])
temp[t++] = nums[i++];
else
temp[t++] = nums[j++];
}
while(i<=mid)//将左边剩余元素填充进temp中
temp[t++] = nums[i++];
while(j<=right)//将右序列剩余元素填充进temp中
temp[t++] = nums[j++];
t = 0;
while(left <= right)//将temp中的元素全部拷贝到原数组中
nums[left++] = temp[t++];
}
};
5.4 算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
堆排序⭐⭐
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
7.1 算法描述
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
7.2 动图演示
7.3 代码实现
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
vector<int> heap(nums.begin(), nums.begin()+k);
buildHeap(heap);
for(int i=k; i<nums.size(); ++i){
if(heap[0]<nums[i]){
heap[0] = nums[i];
heapify(heap, 0);
}
}
return heap[0];
}
private:
void buildHeap(vector<int>& heap){
int n = heap.size();
for(int i=n/2-1; i>=0; --i){
heapify(heap, i);
}
}
void heapify(vector<int>& heap, int idx){
int n = heap.size();
int left = 2 * idx + 1;
int right = 2 * idx + 2;
int i = idx; // smallest
if(left<n && heap[i]>heap[left]){
i = left;
}
if(right<n && heap[i]>heap[right]){
i = right;
}
if(i!=idx){
std::swap(heap[i], heap[idx]);
// recursively reorder affected subtrees
heapify(heap, i);
}
}
};
O(n)========================================
基数排序【稳定】
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
10.2 动图演示
10.3 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
10.4 算法分析
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
9.2 图片演示
9.3 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
9.4 算法分析
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
8.2 动图演示
8.3 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
8.4 算法分析
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。