在学习过程中常常会遇到各种排序的问题,我们通常有思路去解决这个问题,但是在用代码实现的过程中可能会出现各种问题,接下来详细介绍几种常用且好理解的方法,扩展扩展思路。
冒泡排序
假如给一个数组,里面是乱序的一组数字,假设需要将其按照从小到大的顺序排序,冒泡排序是如何做的呢?利用一个简单随机的乱序数组,来探明其原理。
代码如下
void sort(int* p, int a)//传过来的是数组的地址,所以要用指针接收
{
int num = 0;
for (int i = 10; i > 0; i--)//数组下标为0到9
{
for (int j = 0; j < i; j++)
{
if (p[j] > p[j + 1])//第一次比较下标为0位置和1位置的数字大小
{
//如果前一位比后一位大,就把两个数字换换位置
num = p[j];
p[j] = p[j + 1];
p[j + 1] = num;
}//j加1,接下来是第一位和第二位比较,经过9次循环就把最大的数字放在了最后一个位置
}
//跳出循环,接下来是前倒数第二个位置找剩余元素的最大值。
}
}
int main()
{
int i = 0;
int arr[10] = { 3,1,5,2,7,9,8,4,10,6 };//随机数组
sort(arr, 10);//将数组和数组元素个数传递过去
for (i = 0; i < 10; i++)//打印数组
{
printf("%d", arr[i]);
}
return 0;
}
通过上述代码和注释,可以清楚了解冒泡排序的流程,然而冒泡排序的时间复杂度为O(n^2),我们发现这种排序方法是比较慢的,如何将其改进一下呢?
十分巧妙,只需要加一个开关即可。
注意观察
void sort(int* p, int a)//传过来的是数组的地址,所以要用指针接收
{
int num = 0;
for (int i = 10; i > 0; i--)//数组下标为0到9
{
int flag = 0;
for (int j = 0; j < i; j++)
{
if (p[j] > p[j + 1])//第一次比较下标为0位置和1位置的数字大小
{
//如果前一位比后一位大,就把两个数字换换位置
num = p[j];
p[j] = p[j + 1];
p[j + 1] = num;
flag = 1;//如果进入循环,设置flag=1;如果没有进入,
//则说明后边的元素都已经是从大到小排序完了的。
}
//j加1,接下来是第一位和第二位比较,经过9次循环就把最大的数字放在了最后一个位置
}
//跳出循环,接下来是前倒数第二个位置找剩余元素的最大值。
if (flag == 0)
{
break;
}
}
}
就比如十个元素后五个已经排序完成,前五个就已经是从小到大排列的,在第二个循环遍历完后发现没有一次是前一项元素大于后边的元素,那么flag没有被置为1,直接跳出总循环,就可以少循环几次,这一组数越规整,时间复杂度就越接近于O(n)。
选择法排序
选择法排序的排序方法是将没排序过的数组的最大或者最小的一个依次放在未数组的首位,就比如从大到小排序,第一次排序找到最小的放在第一个位置,然后除了已经找出的一个数字,其他剩余数字最小的一个,放在第二个位置,这样直到只剩下最大的一个数,使用选择法排序实现从小到大排序就完成了。
我们可以代码演示从小到大排序。
//这里我们比较注重下标
int main()
{
int arr[] = { 3,1,5,7,9,4,2 };
int num = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < num; i++)
{
int flag = arr[i]; //设置一个数,用来记录最小值
int Pos = i;
for (int j = i + 1; j < num; j++) //j等于i加1,是因为刚开始假设i就等于零
{
if (arr[j] < flag)
{
flag = arr[j]; //如果在后边的遍历中,遇到小于设置的最小值的数,重置flag
Pos = j; //记录最小数字的下标,很重要
}
}
//最后将依次放找到的数和这次遍历的i位置的数互换
arr[Pos] = arr[i];//Pos是剩余数组中最小位置的坐标,把它的值与当前未排序结束的数组的首位互换
arr[i] = flag; //如果是第一次循环,那么arr[0]的位置上就变成了最小值
//下一次循环i++后假设arr[1]是最小值,依次循环
}
for (int k = 0; k < num; k++)
{
printf("%d ",arr[k]);
}
return 0;
}
这种方法的时间复杂度也为O(n^2)。设置一个Pos,记录最小数字的下标,在遍历中不断保存这一个可变的坐标,这样才能将最小的元素和最大的元素交换位置。我们可以在草稿纸上写一串乱序数字,然后看这段代码会异常清晰。
下边也是实现从小到大,但他每次找到最大值放在最后一个位置,同样可以实现从小到大排序的功能。
交换法排序
补充一个交换法排序,这是我们最容易想到且比较好实现的一种排序方法,交换法排序是将每一位数字与其后边的所有数字比较,发现满足条件的,立马交换,从第一个数开始,第一个位置已经确定后,来到第二个位置继续找,发现就交换。
这个实现起来比较简单一点,我们可以看代码
int main()
{
int arr[] = { 3,1,5,7,9,4,2 };
int num = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < num; i++)
{
for (int j = i + 1; j < num; j++)
{
if (arr[j] < arr[i])
{
int iTemp = arr[i];
arr[i] = arr[j];
//找到一个满足条件的,就将arr[i]变成这个数,不断找,不断换
arr[j] = iTemp;
}
}
//完成一次后i++;找到下一个
}
for (int k = 0; k < num; k++)
{
printf("%d ",arr[k]);
}
return 0;
}
我们在第一次排序时,将第一个数依次和后边的数进行比较,找到小于这个数的就互换,然后再往后找,直到遍历整个数组,找到最小值,将这个数放在数组最前边,然后 i 加1,开始放第二个数。直到将这组数实现从小到大排序为止。
插入法排序
使用插入法排序并使数组元素从小到大排列,进入第一次排序时把第一个数取出来,放在第一个位置,然后取第二个数并与第一个数进行比较,如果第二个数小于第一个数,就把第二个数放置在第一个数之前,如果大于第一个数,就放在第一个数后边,然后取第三个数,与第一个数比较,如果小于第一个数,就放在第一个数前边,如果大与第一个数,就与第二个数比较。如果小于第二个数,就放置在第二个数前边,如果大于则放在第二个数后边,然后取第四个数,以此类推,就实现了从小到大排序。
代码如下
int main()
{
int i = 0;
int a[10] = { 0 };
int iTemp = 0;
int iPos = 0;
printf("请输入要排序的值\n");
for (i = 0; i < 10; i++)
{
printf("a[%d]=", i);
scanf("%d", &a[i]);
}
for (i = 1; i < 10; i++)
{
iTemp = a[i];//记录当前取的数的值
iPos = i - 1;//记录当前所取数值的前一个位置的坐标
while ((iPos >= 0) && (iTemp < a[iPos]))//条件是当前取的值大于前面数的值
{
a[iPos + 1] = a[iPos];//如果大于,就把当前位置的前一位的数值赋给当前下标为 i 的位置
iPos--;//再往前判断是否前一位也大于
}
a[iPos + 1] = iTemp;//找到该位置应该在的坐标
//利用前边记录的iTemp给该位置赋值
}
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);//最后是打印
}
return 0;
}
我们要注意,数值是一个一个取出来的,看代码可能比较难以理解,我们假设三个数,来观察代码是如何运行的。
运行后效果如下
折半查找法
折半排序法又称为快速排序,选择一个中间值middle(数组中间值),把比中间值小的数据放在左边,比中间值大的放在右边。然后对两边继续用这种方法,递归实现。
//折半法排序
//实现折半查找法函数的实现
void CelerityRun(int left, int right, int* p)
{
int i, j;
int middle;//数组中间数的值
int iTemp;//换位时用到的临时变量
i = left;
j = right;
middle = p[(left + right) / 2];
do
{
while ((p[i] < middle) && (i < right))//从左找小于中值的数
{
i++;
}
while ((p[j] > middle) && (j > left))//从右找小于中值的数
{
j--;
}
if (i <= j)//找到一对,交换数值
{
iTemp = p[i];
p[i] = p[j];
p[j] = iTemp;
i++;
j--;
}
}
while (i <= j);//如果两边交错,即i大于了j
if (left < j)//递归左半边
{
CelerityRun(left, j, p);
}
if (right > i)//递归右半边
{
CelerityRun(i, right, p);
}
}
int main()
{
int i;
int a[10];
printf("为数组元素赋值");//输入输出数据我们不需再讲了
for (int i = 0; i < 10; i++)
{
printf("a[%d]=", i);
scanf("%d", &a[i]);
}
CelerityRun(0, 9, a);//数组传参
for (int i = 0; i < 10; i++)
{
printf("%d ", a[i]);
}
return 0;
}
运行这串代码,在第一次排序时,获取中间元素,从两侧分别取出数组元素与中间值进行比较,如果从左边取出的第一行个数字比中间值小,则从左边取第二个,如果取得的数比中间数大,就交换两个数组元素值,右侧与左侧刚好相反,在中间值与两边都比较过一遍后,左边数组以第一个元素为起点,以中间值为中点,继续进入函数中,而右边数组以中间值为起点,原来的最后一个元素为终点,进入函数继续进行比较,比较完成后,继续以折半的方式继续进行比较,直到这串数字按照从小到大排列为止。
因为这种方法要使用到递归,所以比较难以理解且难以想到,但这个方法再进行大量数字排序时更快捷一点,可以代入五个无序数组走一遍排序过程,可以使自己更加理解。
接下来进行这几种方法的比较具体在哪种场景下使用哪种方法
1,选择排序
选择法排序在排序过程中一共要经过n(n-1)/2次比较,互相交换n-1次,选择排序法简单且容错率比较高,适合数量较小时的排序。
2,冒泡排序法
最好的情况是顺序刚好是要求的那样,只需要比较一次就好,最坏的情况是要求从小到大排列,而给出的数组元素则是从大到小,这时我们需要比较n^2次,所以冒泡排序待排序数组越有序,效果就越好。
3,交换法排序
类似于冒泡排序,正序时最快,逆序时最慢。排序有序数列时效果最好。
4,插入排序法
插入排序法要经过n-1次插入过程,如果数据恰好应该放在序列的最后端,不需要移动数据,十分节省时间,因此,如果原始数据基本有序,则具有较快的运算速度。
5,折半查找法
对于较大数量的数据,折半排序法是运算速度最快的排序算法,当数量较小时,这种方法往往比其他排序算法还要慢。
总结
插入法,冒泡法,交换法排序的速度比较慢,但当参加排序的序列整体或局部有序时,这几种排序可以达到很快的速度,在这种情况下,折半排序法反而显得有点慢了,当数量较小时,且对稳定性不做要求时,可以用选择法排序,对稳定性有要求时,可以选择冒泡法或者插入法排序。