今天开始介绍我们在工作中经常遇到的算法--排序。排序算法有很多,我们主要介绍以下几种:
我们需要了解每一种算法的定义以及实现方式,并且掌握如何评价一个排序算法。今天我们来学习冒泡排序,插入排序,选择排序。
评价算法的优劣主要从以下几个方面考虑:
第一点毋庸置疑,是我们主要的依判方式;
第二点,我们常常在计算复杂度时会忽略低阶和常系数。这是因为当数据量很大,n趋向于无穷时。但是在实际工作中,我们的数据量不会这么大。所以这些低阶和常数就需要考虑进去了。
第三点:原地排序就是对空间复杂度的一种描述,当排序算法的空间复杂度为O(1)时,我们就称为原地排序
第四点:算法的稳定指的时排序之后是否会将值相同的数据对象改变位置。比如
1,3(1),5,7,9,3(2),这组数据进行排序之后,3(2)还会在3(1)之后吗?
算法的稳定性主要是用于对一组数据进行多次排序时进行参考。比如你要将一组数据按照时间和值大小按照时间从先到后,值从小到大进行排序。一般是按照时间排序,在对值进行排序。但是如果在进行值排序时,使用的是不稳定的算法,那么就会出现问题。(值相等,但时间可能有错误)
冒泡排序原理:
int bubblesort(int a[],int len) |
以上是一般的冒泡排序代码,但是可以稍微优化一下:
int bubblesort(int a[],int len) |
该优化过的代码,加了一个判断标志,当某一次冒泡没有进行数据交互,说明剩下的都是排好序了。故可以直接退出。
分析:
时间复杂度:通过代码实现我们可以知道,冒泡排序的复杂度为O((1+n)*n/2)。
空间复杂度:由于只有一个temp的额外变量,故空间复杂度为O(1),为原地排序。
稳定性:我们在判断时,只要确保a[j]>a[j+1]为判断条件即可保证稳定性。
总结:冒泡排序是原地排序,并且稳定,复杂度为O((1+n)*n/2)的算法。
插入排序原理比较难描述,类似于我们斗地主理牌的过程,原先是乱序的,左边第一个作为依据,依次在乱序中找第一个放到有序中。
int insertsort(int a[],int len) |
分析:
复杂度:通过代码可知,插入排序的复杂度比较难以计算,但可以确定的是O(n^2)。
空间复杂度:O(1),故是原地排序
稳定性:判断条件是a[j] > value,即可保证稳定性
总结:插入排序是原地排序,具有稳定性的算法。
选择排序原理:
其实,我认为选择排序和插入排序类似,它比较的操作是在无序中,在无序中找到最值。插入排序是在有序中比较,将新值放到合适的地方。
int selectsort(int a[],int len) |
分析:
空间复杂度:为O(1),原地排序
时间复杂度:为O(n^2)。
稳定性:不稳定。因为找到最小值之后,需要于无序中的首元素交换,这里会出现不稳定现象。例如:
比如 5,8,5,2,9 这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素 2,与第一个 5 交换位置,那第一个 5 和中间的 5 顺序就变了,所以就不稳定了。
总结:选择排序是原地排序,复杂度为O(n^2),不稳定的算法
归并操作的工作原理如下:
如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
其中的问题就是如何获取两个排好序的部分:当数组长度变为1,是不是就已经相当于排好序了(实际上不需要再排序),这个时候再将长度为1的数组合并到一起,就变成了长度为2的有序数组。
int merge(int a[],int left , int mid, int right) |
其中merge()函数是核心。需要好好品味。
分析:
从归并算法的实现中,我们依旧从稳定性,是否是原地排序?,时间复杂度来分析。
稳定性:我们知道merge()函数包括主要的数据搬移工作,只要保证这里稳定即可。是可以满足的。所以归并排序是稳定算法。
原地排序:在merge()函数中,我们需要将两个有序数组合并,需要用到额外的临时数组,故空间复杂度是O(n),故归并排序不是原地排序
时间复杂度:我们知道归并排序的时间复杂度是O(nlogn),但是至于怎么推导出来的,肯定很多人都处于茫然状态。这里我就稍微推导一下,看不懂没有关系。
假设数据量为n,归并排序的复杂度为T(n),由于归并排序的思路是将大问题分解为小问题, |
故归并排序的时间复杂度为O(n*logn);
快速排序原理:
如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。
经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的
int sort(int a[],int start, int end) |
分析
稳定性:在快速排序中,需要对数组进行搬移,比如:6,8,7,6,3,5,4,会发生不稳定。所以快速排序是不稳定。
原地排序:由于交换数据时,只需要一个临时变量,空间复杂度为O(1),故快速排序是原地排序。
时间复杂度:同归并排序同理,快速排序的事件复杂度也是O(n*logn)。
注意:快速排序 在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n^2)。
问题:O(n) 时间复杂度内求无序数组中的第 K 大元素?
思路:一般情况下,我们需要先进行排序,再通过下标直接访问。虽然数组的访问复杂度是O(1),但是排序较为耗时,即使使用归并排序和快速排序也是O(n*logn)。
既然该题出现在这里,很容易想到会用到归并排序和快速排序。比较一下两者实现的原理,发现快速排序比较适合这道题。当我们sort()返回的mid等于K-1。是不是就表示a[mid],就是第K大的元素(mid左边由K-1个数,都比mid小。虽然不是有序,但没有影响)。当mid+1 分析:为什么该解法的复杂度是O(n)呢? 假设复杂度为T(n),我们已知sort的复杂度是O(n),当我们第一次没有找到正确元素是,我们只需在n/2个数据中进行查找。同理接下来的复杂度是n/4,n/8...直至数组长度为1,及找到对应数据(最坏情况)。这很明显是等比数列。T(n)=2n-1=O(n)。 桶排序原理:核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。 复杂度为什么是O(n)? 如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。 注意: #include 分析 由于数据源较为简单和均匀,所以我选择间隔为5作为映射函数。将数据放到[0,4]5个桶中,再将每个桶中的数据进行排序,再一次将五个桶中的数据依次输出。 桶排序只要映射函数合理,那么其复杂度是O(n),这一点我们已经介绍过了。 桶排序需要额外的内存用于桶,空间复杂度是O(n+m),其中m是桶的个数。 桶排序在将数据隐射到桶的过程是稳定的。但是在排序的过程中如果你选择的是快速排序,就不是稳定的了,如上面的例子。若选择归并排序,就是稳定排序。 计数排序和桶排序的原理类似,更像是特例: 当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 k,我们就可以把数据划分成k个桶。每个桶内的数据值都是相同的,省掉了桶内排序的时间。 int findmaxmin(int a[],int len, int*max ,int *min) 分析 计数排序的时间复杂度为O(n),在算法的过程中涉及到了4个循环.第一个是在数据源中找到最大最小值;第二次循环是遍历数据源,统计数据出现的次数;第三次遍历确认数据的位置;第四次遍历,是进行排序。 计数排序的空间复杂度是O(n+max-min)。故不是原地排序。 计数排序在进行排序时在第四个循环中进行的。若判断条件为for(i = len -1 ; i > 0 ; i-- ),排序就是稳定的。否则就不是稳定的。 注意:计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。 基数排序原理:其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。 我觉得基数排序和计数排序类似,不过计数排序要求数据范围max和min的差距不要太大。但是当数据范围很大时,很明显计数排序和桶排序就不能在使用了。而基数排序就是从位的角度切入到计数排序。 #include 分析 基数排序的时间复杂度是O(n)。 基数排序的空间复杂度是O(n),相对于前两者,减少了一些,所以不是原地排序。 基数排序涉及到数据的搬移和计数排序相似,故基数排序也是稳定排序。 注意:基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。 本章我们接触了冒泡排序,插入排序,选择排序,了解了其定义和代码实现 也介绍了如何评价排序算法的依据。其中冒泡排序和插入排序是稳定的,选择排序是不稳定的。 冒泡排序和插入排序相比较而言,其中的交换数据操作较多: 冒泡: C 插入: C 故综上所述,这三个算法的优先级:插入排序>冒泡排序>选择排序。 归并排序和快速排序,两者的时间复杂度都是O(n*logn)。但是由于归并排序并不是原地排序,所以消耗内存较多。而快速排序是原地排序,解决了归并排序消耗内存较多的问题。快速排序是不稳定的算法,归并排序是稳定算法。 归并排序在任何情况下,时间复杂度都是O(nlogn);快速排序虽然在最坏情况下的时间复杂度为O(n^2),但大部分情况下的复杂度都是O(nlogn)。 桶排序,计数排序,基数排序。它们的空间复杂度都是O(n),因为它们不涉及到比较操作(每个元素之间比较),并且利用了额外的空间,得到了很高的效率。虽然这三种排序拥有很高的效率,但是它们的适用情景较少,对数据要求较为严苛。 桶排序:要求数据均匀分布,或者能够给予一个优秀的映射函数。 计数排序:要求数据源中最大值和最小值的范围较小。 基数排序:要求数据源是正正数,并且范围不能太大。
int sort(int a[],int start, int end)
{
int target = a[end];
int i = start;
int j = start;
int temp = 0;
for(j = start ; j < end ; j++)
{
if(a[j] < target)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
i++;
}
}
temp = a[i];
a[i]=target;
a[end] = temp;
return i;
}
int findKnum(int a[], int start ,int end,int K)
{
int mid = sort(a,start,end);
int result = 0;
if(mid+1 == K )
{
result= a[mid];
}
else if(mid+1 < K)
{
result = findKnum(a,mid+1,end,K);
}
else
{
result = findKnum(a,start,mid-1,K);
}
return result;
}
int main()
{
int a[10]={1,3,5,7,4,9,10,8,6,2};
printf("%d\n",findKnum(a,0,9,6));
return 0;
}桶排序
#include
#include
int sort(int a[],int start, int end)
{
int target = a[end];
int i = start;
int j = start;
int temp = 0;
for(j = start ; j < end ; j++)
{
if(a[j] < target)
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
i++;
}
}
temp = a[i];
a[i]=target;
a[end] = temp;
return i;
}
int quicksort(int a[], int start, int end)
{
if(start >= end)
return 0;
int mid = sort(a,start,end);
quicksort(a,start,mid-1);
quicksort(a,mid+1,end);
}
int insertnum(int a[],int len,int num)
{
int i = 0;
while(i
if(a[i]==0)
{
a[i] = num;
break;
}
i++;
}
return 0;
}
int main()
{
int a[20]={1,3,5,7,4,9,10,8,6,2,12,13,15,14,17,16,19,18,20,11};
//
int buket[5][5] = {0};
int i = 0;
for(i = 0 ; i < 20 ; i++)
{
insertnum(buket[a[i]/5],5,a[i]);
}
for(i = 0 ; i < 5 ;i ++)
quicksort(buket[i],0,4);
int j = 0;
for(i = 0 ; i < 5 ; i ++)
{
for(j = 0 ; j < 5 ; j ++)
buket[i][j] == 0 ? :printf("%d\n",buket[i][j]);
}
return 0;
}计数排序
{
if(len == 0)
return 0;
int i = 0;
*max = a[0];
*min = a[0];
for(i = 0; i < len ; i++)
{
if(a[i] > *max)
*max = a[i];
else if(a[i] < *min)
*min = a[i];
}
return 0;
}
int countsort(int a[],int len)
{
int max,min = 0;
findmaxmin(a,len,&max,&min);
int *count = (int *) malloc((max-min+1)*sizeof(int));
memset(count,0,(max-min+1)*sizeof(int));
int i = 0 ;
for(i = 0 ; i < len ; i++)
count[a[i]-min]++; /
*count*
/
for(i = 1 ; i < max-min+1 ; i++)
count[i] += count[i-1];
int
*target = (int *
**) malloc(len**
sizeof(int));
* memset(target,0,len*
sizeof(int));
for(i = 0 ; i < len ; i++)
{
target[--count[a[i] - min]] = a[i];
}
memcpy(a,target,sizeof(int)*len);
free(count);
free(target);
}
int main()
{
int a[20]={10,12,11,13,13,15,14,12,11,14,15,15,14,13,12,11,13,12,10,10};
countsort(a,20);
int i = 0;
for(i = 0 ; i < 20 ; i ++)
{
printf("%d\n",a[i]);
}
return 0;
}基数排序
#include
#include
int findmaxbit(int a[],int len)
{
if(len == 0)
return 0;
int bit = 0;
int temp= bit;
int i = 0 ;
int num = 0;
for(i = 0 ; i < len ; i++)
{
num = a[i];
temp = 0;
while(num != 0 )
{
num/=10;
temp++;
}
if(temp > bit)
bit = temp;
}
return bit;
}
int basesort(int a[],int len)
{
int max,min = 0;
int bit = findmaxbit(a,len);
int
*temp = (int*
)malloc(sizeof(int)*len);
int count[10] = {0};
int i = 0;
int j = 0;
int radix = 1;
for(j = 0 ; j < bit ; j++)
{
memset(count,0,sizeof(int)*10);
memset(temp,0,sizeof(int)*len);
for(i = 0 ; i < len ; i++)
count[((int) a[i]/radix) %10]++;
for(i = 1 ; i < 10 ; i++)
count[i]+= count[i-1];
for(i = len -1 ; i >= 0 ; i--)
temp[--count[((int) a[i]/radix) %10]]=a[i];
memcpy(a,temp,sizeof(int)
len);
* radix*
=10;
}
free(temp);
return 0;
}
int main()
{
int a[10]={56,123,12315,5312366,1234783,245671,123421,568421,643261,93458};
basesort(a,10);
int i = 0;
for(i = 0 ; i < 10 ; i ++)
{
printf("%d\n",a[i]);
}
return 0;
}总结
if(a[j] > a[j+1])
{
flag = 1;
temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
if (a[j] > value)
{
a[j+1] = a[j]; // 数据移动
} else
{
break;
}