1 分类
2 算法复杂度
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡排序 | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3) | O(1) | 不稳定 |
快速排序 | O(nlogn) | O(logn~n) | 不稳定 |
归并排序 | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n*k) | O(n+k) | 稳定 |
3 简单算法
3.1 冒泡排序
3.1.1 算法描述
冒泡排序是一种简单的排序算法,它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序不满足要求则进行交换。
具体来说,以升序为例,冒泡排序算法的运转如下:
1.比较相邻元素,如果第一个比第二个大,就交换它们两个。
2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大数。
3.针对除了最后元素的其他所有元素重复以上步骤,直到没有任何一对数字需要进行比较。
3.1.2 动图演示
3.1.3 代码实现
void BubbleSort(int[] nums){
for(int i=0;i<nums.Length;i++){
bool isOver = true; //剪枝:如果不存在一对数顺序不符合要求,则排序完成
for(int j=0;j<nums.Length-i-1;j++){
if(nums[j]>nums[j+1]){
Swap(nums,j,j+1);
isOver = false;
}
}
if(isOver) break;
}
}
3.1.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
3.2 简单选择排序
3.2.1 算法描述
简单选择排序是一种简单直观的排序算法,它的工作原理是每一次从待排序的数据元素中选出最小/最大的元素,放在序列的起始位置。然后再对剩余未排序元素进行相同步骤。
具体来说,以升序为例,简单选择排序算法的运转如下:
1.初始时,待排序序列为nums[0…n-1]。
2.从待排序序列中选出最小元素,与待排序序列的起始位交换。
3.除开起始位,剩余元素组成新待排序序列,重复步骤2,直到待排序序列为空(其实剩余1个数就可以停止了)。
3.2.2 动图演示
3.2.3 代码实现
void SelectSort(int[] nums){
for(int i=0;i<nums.Length-1;i++){
int min = i;
for(int j=i+1;j<nums.Length;j++){
if(nums[j]<nums[min]) min = j;
}
if(min!=i) Swap(nums,min,i);
}
}
3.2.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 不稳定 |
3.3 直接插入排序
3.3.1 算法描述
直接插入排序是一种简单直观的排序算法,它的工作原理是构建有序序列,对于未排序数据,在已排序序列中从后往前扫描,找到相应位置并插入,可以想象成打牌过程中的理牌过程。
具体来说,以升序为例,直接插入排序算法的运转如下:
1.将第一个元素视为初始有序序列。
2.从有序序列末位元素的下一位开始从后向前扫描。
3.如果当前元素(已排序序列中的元素)大于新元素,则将该元素后移一位。
4.重复步骤3,直到当前元素小于或等于新元素。
5.将新元素插入到该元素后面。
6.重复步骤2~5。
3.3.2 动图演示
3.3.3 代码实现
void InsertSort(int[] nums){
for(int i=1;i<nums.Length;i++){
int temp = nums[i];
int j;
for(j=i-1;j>=0;j--){
if(nums[j]>temp) nums[j+1] = nums[j];
else break;
}
nums[j+1] = temp;
}
}
3.3.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
4 改进算法
4.1 希尔排序
4.1.1 算法描述
希尔排序是插入排序的一种更高效的改进版本。该方法因 D.L.Shell 于 1959 年提出而得名。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个序列恰被分成一组,再对全体记录进行直接插入排序。
希尔排序的时间复杂度与增量序列的选取有关,平均时间复杂度为O(n^1.3)。
4.1.2 动图演示
4.1.3 代码实现
void ShellSort(int[] nums){
int gap = 1;
//PS:关于为什么是gap*3+1而不是gap*3,据说是因为这样比较符合二进制计算机的特点,能够减少乘法运算的次数,从而提高效率;也有说法是说如果不这样做,算法复杂度可能会降到O(n^2)
while(gap<nums.Length) gap = gap*3+1;
while(gap>0){
//直接插入排序
for(int i=gap;i<nums.Length;i++){
int temp = nums[i];
int j;
for(j=i-gap;j>=0;j-=gap){
if(nums[j]>temp) nums[j+gap] = nums[j];
else break;
}
nums[j+gap] = temp;
}
gap/=3;
}
}
4.1.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( N 1.3 ) O(N^{1.3}) O(N1.3) | O ( 1 ) O(1) O(1) | 不稳定 |
4.2 快速排序
4.2.1 算法描述
快速排序是一种基于分治思想的排序算法,它不断地将待排序数组分成两个子数组,将两部分独立地排序。
具体来说,以升序为例,快速排序的运转如下:
1.选择基准元素pivot:从数组中选择一个元素作为基准元素。
2.分割操作:将所有比基准元素小的元素放在它前面,所有比基准元素大的元素放在它后面。
3.递归地对左右子数组进行快速排序。
4.2.2 动图演示
4.2.3 基准值选择
快速排序的基准值选择有三种方法:
1.端点作为基准值。
2.随机值作为基准值。
3.三数取中法。
其中,三数取中法是最常用的基准值选择方法。该方法首先从待排序数组的left、mid、right位置上的数据,选出其中大小居中的一位作为基准值。这样就可以避免在数组已经有序或者近乎有序的情况下,快速排序退化成冒泡排序。
4.2.4 代码实现
1 端点作为基准值
void QuickSort(int[] nums,int left,int right){
if(left>=right) return;
int mid = PartSort(nums,left,right);
QuickSort(nums,left,mid-1);
QuickSort(nums,mid+1,right);
}
int PartSort(int[] nums,int left,int right){
//最简单的基准值选择:选择待排序数组的第一位元素。
int pivot = nums[left];
while(left<right){
while(left<right&&nums[right]>=pivot) right--;
nums[left] = nums[right];
while(left<right&&nums[left]<=pivot) left++;
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
2 随机值作为基准值
//C# Random实例.Next左闭右开
Random random = new Random();
int index = random.Next(left,right+1);
Swap(nums,index,left);
//Unity Random.Range左闭右开
int index = Random.Range(left,right+1);
Swap(nums,index,left);
3 三数取中法
//在int pivot = nums[left]前加上以下代码,使数值居中的值位于left位。
int mid = (left+right)/2;
if(nums[left]>nums[right]) Swap(nums,left,right);
if(nums[mid]>nums[right]) Swap(nums,mid,right);
if(nums[left]<nums[mid]) Swap(nums,left,mid);
4.2.5 优化
优化1:序列长度达到一定大小时,使用插入排序。
当快排进行到一定深度后,划分的区间很小,这时再使用快速排序的效率不高,而此时使用插入排序能避免一些有害的退化情形。
void QuickSort(int left,int right){
if(left>=right) return;
int len = right-left+1;
if(len<10) InsertSort(left,right);
else{
int mid = PartSort(left,right);
QuickSort(left,mid-1);
QuickSort(mid+1,right);
}
}
优化2:尾递归优化
快排算法和大多数分治排序算法一样,都有两次递归调用。但是快排与归并排序不同,归并的递归则在函数一开始, 快排的递归在函数尾部,这就使得快排代码可以实施尾递归优化。使用尾递归优化后,可以缩减堆栈的深度,由原来的O(n)缩减为O(logn)。
尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
一个例子
//线性递归
int fact(int n){
if(n<0) return 0;
else if(n==0||n==1) return 1;
else n*fact(n-1);
}
n=5时,线性递归的递归过程如下:
fact(5)
{5*fact(4)}
{5*{4*fact(3)}}
{5*{4*{3*fact(2)}}}
{5*{4*{3*{2*fact(1)}}}}
{5*{4*{3*{2*1}}}}
{5*{4*{3*2}}}
{5*{4*6}}
{5*24}
120
//尾递归
int fact(int n,int a){
if(n<0) return 0;
else if(n==0) return 1;
else if(n==1) return a;
else return fact(n-1,a*n);
}
n=5时,尾递归的递归过程如下:
facttail(5,1)
facttail(4,5)
facttail(3,20)
facttail(2,60)
facttail(1,120)
120
可见,尾递归能很好地降低栈深。
快排优化
第一次递归以后,变量left就没有用处了, 也就是说第二次递归可以用迭代控制结构代替。
void QuickSort(int left,int right){
if(left>=right) return;
int len = right-left+1;
if(len<10) InsertSort(left,right);
else{
while(left<right){
int mid = PartSort(left,right);
QuickSort(left,mid-1);
left = mid+1;
}
}
}
4.2.6 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n l o g n ) O(nlogn) O(nlogn) | O ( l o g n − n ) O(logn-n) O(logn−n) | 不稳定 |
4.3 归并排序
4.3.1 算法描述
归并排序是一种基于分治思想的排序算法,它将一个大的数组先拆分为几个小的数组,然后再一点点地合并。归并排序的基本思想是将待排序序列分为若干个子序列,每个子序列都是有序的,然后再将子序列合并成整体有序序列。
具体来说,以升序为例,归并排序算法的运转如下:
1.将待排序序列分为若干个子序列。
2.将相邻的子序列进行合并,得到若干个长度为2的有序序列。
3.重复步骤2。
4.3.2 快速排序和归并排序的区别
快速排序和归并排序都采用了分治的思想,它们的算法时间复杂度均为O(nlogn),但具体的实现方式不同。
快速排序是一种不稳定的排序算法,它的基本思想是通过一趟排序将待排序列分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,然后再按此方法对这两部分记录继续进行排序,以达到整个序列有序的目的。并且,快速排序是一种原地排序算法,它通过交换数组中的元素来实现排序,快速排序的空间复杂度取决于递归栈的深度,一般来说空间复杂度为O(logn)~O(n)。
归并排序是一种稳定的排序算法,它需要额外的空间来存储临时数组。归并排序的基本思想是将待排序序列分成若干个子序列,每个子序列都是有序的,然后再将子序列合并成整体有序序列。归并排序的空间复杂度为O(n)。
4.3.3 动图演示
4.3.4 代码实现
void MergeSort(int[] nums,int left,int right){
if(left>=right) return;
int mid = (left+right)/2;
MergeSort(nums,left,mid);
MergeSort(nums,mid+1,right);
Merge(nums,left,right);
}
void Merge(int[] nums,int left,int right){
int mid = (left+right)/2;
int i = left;
int j = mid+1;
int[] temp = new int[right-left+1];
int k = 0;
while(i<=mid&&j<=right){
if(nums[i]<=nums[j]) temp[k++] = nums[i++];
else temp[k++] = nums[j++];
}
while(i<=mid) temp[k++] = nums[i++];
while(j<=right) temp[k++] = nums[j++];
for(int p=0;p<temp.Length;p++) nums[left+p] = temp[p];
}
4.3.5 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n l o g n ) O(nlogn) O(nlogn) | O ( n ) O(n) O(n) | 稳定 |
4.4 堆排序
4.4.1 算法描述
堆排序是一种原地排序算法,它通过维护一个堆来实现排序。堆是一种特殊的树形数据结构,它满足以下两个条件:
1.堆中某个节点的值总是不大于或不小于其父节点的值。
2.堆总是一棵完全二叉树。
堆排序的基本思想是将待排序序列构造成一个大根堆或小根堆,然后依次取出堆顶元素,直到整个序列有序为止。
具体来说,以升序为例,堆排序算法的运转如下:
1.建堆:将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此时(R1,R2….Rn)为初始无序区。
2.交换:将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
3.调整堆:由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
4.4.2 动图演示
4.4.3 代码实现
知识点:
父:i -> 子:2i+1/2i+2
子:i -> 父:(i-1)/2
void HeapSort(int[] nums){
CreateHeap(nums);
for(int i=nums.Length-1;i>0;i--){
Swap(nums,0,i);
AdjustHeap(nums,0,i-1);
}
}
void CreateHeap(int[] nums){
int last = nums.Length-1;
for(int i=(last-1)/2;i>=0;i--){
AdjustHeap(nums,i,last);
}
}
void AdjustHeap(int[] nums,int left,int right){
int root = left;
int child = root*2+1;
while(child<=right){
if(child+1<=right&&nums[child]<nums[child+1]) child++;
if(nums[root]>=nums[child]) return;
else{
Swap(nums,root,child);
root = child;
child = root*2+1;
}
}
}
4.4.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n l o g n ) O(nlogn) O(nlogn) | O ( 1 ) O(1) O(1) | 不稳定 |
5 其他算法
5.1 计数排序
5.1.1 算法描述
计数排序的核心在于将输入的数据转换为数组下标存放在额外开辟的数组空间中,它要求输入的数据必须是有确定范围的正整数。
具体来说,以升序为例,计数排序算法的运转如下:
1.从无序序列中找出最大值max和最小值min,确定额外开辟的数组空间的大小:int[] temp = new int[max-min+1]。
2.遍历原数组,统计数组中每个值出现的次数,并记录在数组newArr中。
3.完善统计数组:统计数组的每一项为newArr到当前一项的计数总和。
4.反向填充。
5.1.2 动图演示
5.1.3 代码实现
void CountSort(int[] nums){
//1.找最大最小值以确定额外开辟的数组空间的大小
int min = nums[0];
int max = nums[0];
for(int i=1;i<nums.Length;i++){
if(nums[i]<min) min = nums[i];
if(nums[i]>max) max = nums[i];
}
int[] newArr = new int[max-min+1];
for(int i=0;i<nums.Length;i++){
newArr[nums[i]-min]++;
}
//2.统计数组-为保证排序稳定
int[] countArr = new int[newArr.Length];
for(int i=0;i<newArr.Length;i++){
if(i==0) countArr[i] = newArr[i];
else countArr[i] = newArr[i]+countArr[i-1];
}
//3.最终结果
int[] result = new int[nums.Length];
for(int i=nums.Length-1;i>=0;i--){
result[countArr[nums[i]-min]-1] = nums[i];
countArr[nums[i]-min]--;
}
}
5.1.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
5.2 桶排序
5.2.1 算法描述
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
具体来说,以升序为例,桶排序算法的运转如下:
1.首先找出所有数据中的最大值max和最小值min。
2.确定桶的大小:根据max和min确定每个桶所装数据的范围size,size = (max-min)/n+1,n为数据的个数,需要保证至少有一个桶,故而需要加个1。
3.确定桶的个数:求得了size即知道了每个桶所装数据的范围,还需要计算出所需的桶的个数cnt,cnt = (max-min)/size+1,需要保证每个桶至少要能装1个数,故而需要加个1。
4.求得了size和cnt,即可知第一个桶装的数据范围为[min,min+size),第二个桶为[min+size,min+2*size),…,以此类推。
5.对各个桶中的数据进行排序,可以递归使用桶排序,也可以使用其他排序方法。
6.将各个桶中有序序列依次输出。
5.2.2 代码实现
void BucketSort(int[] nums){
//1.找最小值和最大值以计算size和cnt
int min = nums[0];
int max = nums[0];
for(int i=1;i<nums.Length;i++){
if(nums[i]<min) min = nums[i];
if(nums[i]>max) max = nums[i];
}
int n = nums.Length;
int size = (max-min)/n+1;
int cnt = (max-min)/size+1;
List<int>[] bucket = new List<int>[cnt];
for(int i=0;i<cnt;i++){
bucket[i] = new List<int>();
}
//2.扫描数组,将元素放进对应的桶里
for(int i=0;i<nums.Length;i++){
int index = (nums[i]-min)/size;
bucket[index].Add(nums[i]);
}
//3.对各个桶进行排序
for(int i=0;i<cnt;i++){
bucket[i].Sort();
}
//4.反向填充
int m = 0;
for(int i=0;i<cnt;i++){
for(int j=0;j<bucket[i].Count;j++){
nums[m++] = bucket[i][j];
}
}
}
5.2.3 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n + k ) O(n+k) O(n+k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
5.3 基数排序
5.3.1 算法描述
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。
具体来说,以升序为例,基数排序算法的运转如下:
1.找出数组中的最大数,确定位数。
2.从最低位开始每个位组成radix数组。
3.对radix数组进行计数排序。
5.3.2 动图演示
5.3.3 代码实现
void RadixSort(int[] nums) {
//1.准备桶
int[,] bucket = new int[10, nums.Length];
int[] bucketCount = new int[10];
//2.获取最大数的位数
int max = nums[0];
for(int i = 1; i < nums.Length; i++) {
if (nums[i] > max) max = nums[i];
}
int len = (max + "").Length;
//3.桶排序
int arrCount; //原始数组索引
for(int k = 0, n = 1; k < len; k++, n *= 10) {
//3.1 第k轮桶排序
for(int i = 0; i < nums.Length; i++) {
int digit = nums[i] / n % 10; //获取个十百千万数
bucket[digit,bucketCount[digit]] = nums[i];
bucketCount[digit]++;
}
//3.2 将桶中数据取出来给原数组
arrCount = 0;
for(int i = 0; i < 10; i++) {
if (bucketCount[i] != 0) {
for(int j = 0; j < bucketCount[i]; j++) {
nums[arrCount++] = bucket[i, j];
}
}
}
//3.3 清空桶
bucketCount = new int[10];
}
}
5.3.4 性能分析
时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|
O ( n ∗ k ) O(n*k) O(n∗k) | O ( n + k ) O(n+k) O(n+k) | 稳定 |
6 List的排序方法
6.1 排序方法
list.Sort(); //升序方法
list.Reverse(); //降序方法
list.OrderBy(x=>x.MyProperty); //OrderBy接受一个Lambda表达式指定要按其排序的键
6.2 改写排序方法
6.2.1 x.CompareTo(y)
int result = a.CompareTo(b);
如果a=b,那么result=0;如果a>b,那么result=1;如果a<b,那么result=-1。
6.2.2 改写
1 自定义比较器
class CustomComparer : IComparer<MyClass>{
public int Compare(MyClass x,MyClass y){
return x.MyProperty.CompareTo(y.MyProperty);
}
}
list.Sort(new CustomComparer());
2 Lambda
list.Sort((x,y)=>x.MyProperty.CompareTo(y.MyProperty));
3 匿名委托
list.Sort(delegate(MyClass x,MyClass y){
if(x.MyProperty>y.MyProperty) return 1;
else return -1;
});
7 练习
912 排序数组
略,可以用来练习手撕排序。
56 合并区间
public class Solution {
public int[][] Merge(int[][] intervals) {
//1.依据左端点进行排序
Array.Sort(intervals,(x,y)=>x[0].CompareTo(y[0]));
//2.合并重叠区间
List<int[]> list = new List<int[]>();
list.Add(intervals[0]);
for(int i=1;i<intervals.Length;i++){
int[] temp = list[list.Count-1];
if(temp[1]>=intervals[i][0]){
temp[1] = Math.Max(temp[1],intervals[i][1]);
}else{
list.Add(intervals[i]);
}
}
return list.ToArray();
}
}
148 排序链表
1 找链表中点-快慢指针
ListNode FindMidNode(ListNode head){
if(head==null||head.next==null) return head;
ListNode fast = head;
ListNode slow = head;
while(fast.next!=null){
if(fast.next.next==null) return slow;
else fast = fast.next.next;
slow = slow.next;
}
return slow;
}
2 合并两个有序链表
ListNode Merge(ListNode list1,ListNode list2){
if(list1==null) return list2;
if(list2==null) return list1;
if(list1.val>list2.val) return MergeTwoLists(list2,list1);
list1.next = MergeTwoLists(list1.next,list2);
return list1;
}
3 总结
public class Solution {
public ListNode SortList(ListNode head) {
if(head==null||head.next==null) return head;
ListNode mid = MiddleNode(head);
ListNode temp = mid.next;
mid.next = null;
ListNode list1 = SortList(head);
ListNode list2 = SortList(temp);
return MergeTwoLists(list1,list2);
}
//辅助方法1:找到链表的中间结点(当中间结点有两个时,返回前面的那个)
private ListNode MiddleNode(ListNode head){
if(head==null||head.next==null) return head;
ListNode fast = head;
ListNode slow = head;
while(fast.next!=null){
if(fast.next.next==null) return slow;
else fast = fast.next.next;
slow = slow.next;
}
return slow;
}
//辅助方法2:合并两个有序链表
private ListNode MergeTwoLists(ListNode list1,ListNode list2){
if(list1==null) return list2;
if(list2==null) return list1;
if(list1.val>list2.val) return MergeTwoLists(list2,list1);
list1.next = MergeTwoLists(list1.next,list2);
return list1;
}
}