赞
踩
对于一个给定包含n个元素的数组,我们总需要将其按照相对大小进行排序,进而方便进行数据处理。如排序学生的成绩,员工的薪水。这就设计到了排序算法
今天我学习的是三种初等排序方法——插入排序、冒泡排序、希尔排序
对于不同的排序算法,我们有几个重要的关注点:
1.时间复杂度:是数组元素n的函数,越低效率越高
2.排序的稳定性:当数据中存在2个或2个以上的数值相等的元素时,这些元素在排序处理前后的顺序不变,被称为排序的稳定性
想象自己正在打扑克牌,一开始收到的一手牌是无序的,若我们需要将其由小到大地排序,则我们从左往右检索每一张牌,当找到的牌比前面的牌数值要小时,就插入到前面去,逐步进行直到手牌有序
插入排序就是模拟这样的一个过程
假定我们现在要将数组由小到大排序
我们将数组从左到右划分为有序部分和无序部分
每次检索无序部分的第一个元素(称其为a),并将其逐渐往前与有序部分作比较,当找到第一个比a小的元素(称其为b)时,将a插到b的下一位。届时,我们就多排好了一个元素。继续循环下去,直到将无序部分的元素全部插入有序部分,即完成了对数组的排序
由于一开始数组无序,我们认为第一个元素有序即可
如何将a插入到b的下一位呢?我们只需要将有序部分中比a大的元素全都往后移一位即可(直接覆盖后一位)
实现代码如下:
- void InsertSort(int* nums, int numsSize){
- for(int i=1;i<numsSize;i++){//认为nums[0]有序
- int j=i-1;//从nums[i]的前一位开始查找,即有序部分的最后一位
- int check=nums[i];
- while(j>=0&&nums[j]>check){
- nums[j+1]=nums[j];//向后移动一位,覆盖原元素
- j--;
- }
- nums[j+1]=check;//找到比nums[i]小的元素nums[j]后,在其下一位插入nums[i]
- }
- }
研究排列数组的不同情况,我们有:
当插入排序对完全逆序的数组进行排序时,每一个元素都要向前插入,此时其时间复杂度为O()
当插入排序对完全顺序的数组进行排序时,每一个元素只需检索一次,此时其时间复杂度为O(n)
在插入排序中,我们只将比check大的元素向后平移,不相邻的元素不会直接交换位置,因此整个排序算法十分稳定
冒泡排序法就是让数组元素像水中的气泡一样逐渐上浮,从而达到排序的目的。
冒泡排序同样将数组分为有序部分与无序部分,并假定一开始的有序部分为空
假定我们要将数组由小到大排序
则每检索一次无序部分,我们就当将其中的最大元素上浮(到数组最右边)到有序部分
如此循环,我们就可以将第二大、第三大的元素逐个排到有序部分,进而完成数组的排序
那们如何找到无序部分的最小值并将其上浮呢?
我们采用相邻元素对比并交换的方法:
对于给定的数组{a,b,c,d,e,f,g}
研究元素a与b,若a>b,则将a与b位置交换,从而使较大的a向右移动一位
接着再比较a与c,如此循环,从而将数组中的最大值移动到数组最右边
我们只需要循环n次上述对无序部分检索的步骤,即可将数组排序
至此,我们可以写出实现代码:
- void BubbleSort(int* nums, int numsSize){
- for(int i=0;i<numsSize;i++){ //i可以表示已排序的元素个数
- for(int j=0;j<numsSize-1-i;j++){//无序部分长度为数组长度-有序部分长度
- if(nums[j]>nums[j+1]){ //再-1是为了保证[j+1]不溢出
- int tem=nums[j+1];
- nums[j+1]=nums[j];
- nums[j]=tem; //交换位置
- }
- }
- }
- }
该算法的时间复杂度为O()
因为冒泡排序仅对数组中的相邻元素进行比较和交换,因此数值相同的元素并不会改变顺序,所以冒泡排序具有稳定性
tips:若将if中的判断条件的>改为>=,则数值相等时两个元素会交换位置,此时不再具有稳定性
前面两种算法的时间复杂度最高都为O(),当n很大时,时间消耗很高
那么能不能对其进行一些优化,尽量减少其时间复杂度呢?
我们把目光看向插入排序,发现,当被排列的数组越接近有序时,插入排序所需要的时间越少
那么,我们能否设计一种算法,在使用插入排序之前,使数组尽量有序?
这就是希尔排序的设计思路
假定给出一个数组{2,5,3,8,9,6,3,4,7,1},个数n=10
我们先对其进行预处理:
按照一定的间隔gap,将其分为几个组
当gap等于5时,我们有:
{2,6} , {5,3} , {3,4} , {8,7} , {9,1}
对这五个小数组,我们将其从小到大排序,于是原数组被改造为:
{2,3,3,7,1,6,5,4,8,9}
再取gap的值为2
我们有:
{2,3,1,5,8} , {3,7,6,4,9}
对其从小到大进行排序,原数组被改造为:
{1,3,2,4,3,6,5,7,8,9}
此时数组已经变得接近有序,我们再使用插入排序,即可完成排序
因为我们在给小数组排序的过程中,已经尽量地将相对小的元素排列在左边,而相对大的元素排列在右边
对不同的数组,我们应当设置不同的gap值,对不同的gap,给小数组排序的总次数就不同,这就会导致代码量的改变,那么我们如何避免这一个问题呢?
观察直接插入排序的实现代码,我们可知,在将无序部分第一个元素b插入有序部分时,比a大的元素全都移到了它们的下一位,这是因为它们的排序是连续的。那么对于存在gap的不连续的小数组而言,我们也只需要对其使用插入排序,并在插入时将其后移到下gap位即可
希尔排序的时间复杂度与gap值的确定方式息息相关,这是一个复杂的问题,涉及到一些数学上未能攻克的难题
最初希尔排序的发明者——希尔提出的gap=n/2,每排序完一次小数组,gap/=2,使gap变为原来的1/2,直到gap==1时变为直接插入排序
后来Knuth提出了gap=(gap/3)+1,使每次排序过后gap变为原来的1/3,(加1是为了防止gap<=3时gap/3==0的情况),在这种情况下,时间复杂度基本维持在O()
在这里我们采用希尔的方法确定gap的值
至此,我们可以写出实现代码如下:
- void ShellSort(int* nums, int numsSize){
- int gap=numsSize;
- while(gap>1){
- gap/=2; //gap的值减少一半
- for(int i=0;i<n-gap;i++){ //通过i所指的元素,确定我们在排序哪个小数组
- int end=i;
- int temp=nums[end+gap]; //end跳跃一个gap,即为等待插入的值temp
- while(end>=0){ //end用于判断是否超出小数组左边
- if(temp<nums[end]){ //temp比小数组中其前一个元素小时,前一个元素向后移动gap尾
- nums[end+gap]=nums[end];
- end-=gap; //检索小数组中更前一个元素
- else{
- break; //当检测到小的,不再前插
- }
- }
- nums[end+gap]=temp; //将temp插到end后第gap个,即小数组中end的下一个
- }
- }
-
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。