一、桶排序以及排序总结
二、今日刷题
一、桶排序以及排序总结
1、快排 快排的核心就是partition(partition)。经典的快排:<经典的快排将数组的最后一个数作为partition的值,对数组的前n-1个数进行partition,然后将大于区域的对第一个数与数组的最后一个数进行交换,那么数组最后一个数的位置就确定了,就这样依次确定每个数的位置从而完成排序。
改进的快速排序:使用荷兰国旗问题的求解方法,还是用数组的最后一个数作为partition的值,将数组partition为小于区域,等于区域和大于区域,然后再将大于区域的第一个值与数组的最后一个值交换;然后再递归对小于区域和大于区域做同样的操作;比较传统的partition方法,荷兰国旗问题的partition方法有改进,但是也是常数项。
时间复杂度分析:快速排序的时间复杂度取决于partition的取值。如果partition值选多的好,那么刚好将小于区域的值和大于区域的值分的差不多,那么T(N) = 2T(N)+O(N),根据主定理可以得到时间复杂度为O(N*logN)。但是如果partition分了之后只有最小区域,例如{1,2,3,4,5,6,7,9},那么时间复杂度就是O(N^2)。而通常的快排时间复杂度之所以能达到O(N*logN),主要的原因是partition值的选取是随机的,那么每个位置的概率为1/n,假设左边分为k/n,右边为(n-k)/n,那么时间复杂度为T(N)=T(K/N)+T((N-K)/N)+O(N),该时间复杂度的概率为1/n,将所有的时间复杂度乘上其概率,将所有的可能性相加得到的时间复杂度的期望就是O(N*logN),所以快排的时间复杂度为O(N*logN)。
代码:
//记录partition之后的小于区域的右边界和大于区域的左边界
struct area
{
int less;
int more;
};
//将数组的最后的元素作为partition值做荷兰国旗问题的partition
//注意对l到r-1的值进行partition之后,还要将r位置上的值放到等于区域的位置
area partition(int a[], int l, int r)
{
int less = l - 1;
int more = r;
int temp;
while (l < more)//当当前的值与大于区域相碰的时候结束
{
//小于区域
if (a[l] < a[r])
{
less++;
temp = a[less];
a[less] = a[l];
a[l] = temp;
l++;
}
//大于区域
else if (a[l]>a[r])
{
more--;
temp = a[more];
a[more] = a[l];
a[l] = temp;
}
//等于区域
else
l++;
}
//最后将a[r]放到等于区域
temp = a[more];
a[more] = a[r];
a[r] = temp;
area res;
res.less = less;
res.more = more;
return res;
}
void quicksort(int a[], int l, int r)
{
int temp;
//注意是递归函数,要设置递归结束的条件
if (l < r)
{
//随机选一个位置作为partition的值
int pos = rand()*(r - l + 1) + l;
temp = a[pos];
a[pos] = a[r];
a[r] = temp;
area temp_area = partition(a, l, r);
quicksort(a, l, temp_area.less);
quicksort(a, temp_area.more, r);
}
}
空间复杂度分析:当是最糟糕的情况,例如{1,2,3,4,5,6,7,8,9},每次将最后一个数作为partition值,那么每次只能确定一个值的位置,且其位置不变,所以要进行n此递归,那么空间复杂度就是O(N);但是当刚好将区域平均分的时候,我们只需要进行logn次的递归,所以空间复杂度为O(logn);我们根据时间复杂度的估计的方式需要考虑每种概率下的情况,最终得到空间复杂度的期望是O(logN)的。
2、基于比较的排序
插入排序、冒泡排序、选择排序、归并排序、快速排序等都是基于比较的排序,基于比较也就是告诉比较的方式进行排序,当我们对结构体等复杂的数据要求按照特定的方式进行比较的时候我们就要指定对应的比较规则,这样我们就使用到了比较器。
C/C++一般在使用sort函数的时候指定比较器。
sort函数的比较器:比较的两个参数,如果返回的是第一个参数<第二个参数,那么就按照从小到大的排序;如果返回的是第一个参数>第二个参数,那么就按照从大到小的顺序排序。
在c语言中有qsort函数,qsort函数的比较器,返回的负数那么就将第一个参数排在前,返回的是正数那么就认为第一个参数排在后,等于0那么无所谓第一个参数和第二个参数排在前或者后。
注意:在定义比较器的排序规则的时候需要保证比较是有传递性的,不能形成环。
例子:对学生的数据按照年龄从小到大排序,年龄相等的按照id从小到大排序。
struct student {
int id;
int age;
}students[5];
int cmp(student x, student y)
{
if (x.age == y.age)//年龄相同的时候按照id从小到大排序
return x.id < y.id;
else
return x.age < y.age;
}
//比较的时候直接调用
sort(students, 0, 4, cmp);
3、桶排序
桶排序不是基于比较的排序,其时间复杂度可以达到O(N),但是使用桶排序需要我们充分了解数据的状况。
步骤:
- 首先找出最大值,看最大值有多少位,然后将所有的数补成相同的位数
- 然后从前往后按照各位入桶
- 再从0到9号桶中一次倒出数据
- 再按十位进行入桶
- 如此重复,直到判断完所有的位数,那么最后一次倒出桶中的数据的顺序就是排好的顺序
- 代码:
int maxbit(int a[],int len)//返回数组中最大的数有多少位
{
int maxvalue = 99999999;
for (int i = 0; i < len; ++i)
{
maxvalue = max(maxvalue, a[i]);
}
int res = 0;
while (maxvalue != 0)
{
res++;
maxvalue = maxvalue / 10;
}
return res;
}
//获得value上的第pos位上的数
int getdigit(int value, int pos)
{
return ((value / ((int)pow(10, pos - 1))) % 10);
}
void bucketsort(int a[], int l, int r, int digit)
{
int count[10];//每一位上的数字由多少
int num;
int* bucket = new int[r - l + 1];
//一共有digit位,那么要进桶这么多次
for (int i = 1; i <= digit; ++i)
{
memset(count, 0, sizeof(count));
for (int j = l; j <= r; ++j)
{
num = getdigit(a[j], i);//得到数字的第i位上的数字
count[num]++;
}
for (int j = 1; j <= 9; ++j)
count[j] += count[j - 1];
//然后根据count之后进桶
//进桶的时候要从后往前进
//因为我们进桶的顺序是相对于前一次出桶后的结果,那么按照前次的结果其实按照d-1为
//从小打到排列的,那么进桶的时候也应该是值大的排在后面,而我们又是通过count--来实现
//进桶的,所以应该从后往前进桶
for (int j = r; j >= l; --j)
{
num = getdigit(a[j], i);
bucket[count[num] - 1] = a[j];
count[num]--;
}
int k = l;
for (int j = l; j <= r; ++j)
a[k++] = bucket[j];
}
}
void radixsort(int a[],int len)
{
bucketsort(a, 0, len-1, maxbit(a,len));
}
int main()
{
int a[5] = { 102,6,99,100,4 };
radixsort(a, 5);
for (int i = 0; i < 5; ++i)
cout << a[i] << endl;
system("pause");
return 0;
}
4、不同的排序算法之间的比较
排序的稳定性:排序的稳定性就是值相同数值的数其经过排序后在数组中的相对位置不变,引入排序的稳定性是因为现实生活中的很多问题会用到排序的稳定性。
例子:每个学生的信息有班级和年龄,首先按照年龄进行排序,在按照班级进行排序,那么最后在同一个班级的同学的年龄是从小到大进行排序的。
不同的排序算法之间的比较
排序算法 | 时间复杂度 | 空间复杂度 | 是否为稳定的排序算法 |
---|---|---|---|
选择排序 | O(N^2) | O(1) | No,例如{3,3,3,1,3,3},首先扫描到最小值为1,然后将1与0位置上的3交换,之后就不稳定了 |
冒泡排序 | O(N^2) | O(1) | YES |
插入排序 | O(N^2) | O(1) | YES |
归并排序 | O(N*logN) | O(N) | 取决于merger函数的时候出现值相等的时候先选择左边的值还是右边的值 |
快排 | O(N*logN) | O(logN) | NO,partition的过程就是不稳定的 |
堆排序 | O(N*logN) | O(1) | NO |
桶排序 | O(N) | O(N) | YES |
分析:不存在,因为要么奇数要么偶数是0,1标准,和快排的partition过程是一直,但是快排也只能到O(N*logN)并且其不稳定,所以是做不到的。
注意:目前还没有找到时间复杂度低于O(N*logN)的基于比较的排序算法。
5、工程上排序算法的改进
工程上可以在同一个问题中结合不同排序算法的优势,从稳定性和时间复杂度角度进行分析。
二、今日刷题
题目:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。思路:使用动态规化,d[n]表示的是走n个台阶的方法数,那么在最后一步可以走两个台阶或者走一个台阶,所以递推公式为d[n]=d[n-1]+d[n-2];初始条件为d[1]=1,d[2]=2。
代码:在桶排序的实现中,我们借助了count数组巧妙实现了出桶和进桶的过程
int jumpFloor(int number) {
int d[10000];
memset(d,0,sizeof(d));
d[1] = 1;
d[2] = 2;
if(number>2)
{
for(int i = 3;i<=number;++i)
d[i] = d[i-1]+d[i-2];
}
return d[number];
}