排序技术真是太重要了,最近我也在看这个,所以今天发在博客来总结一下,自己对排序技术的看法和理解。
我们先做个准备,写一个无序的数组:
我们首先来看看基础的排序,最基础的就是冒泡排序了。
冒泡排序:
先帖代码:
public int[] BubbleSort(int[] TargetArray,int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (TargetArray[j + 1] < TargetArray[j])
{
int UseSave = TargetArray[j+1];
TargetArray[j + 1] = TargetArray[j];
TargetArray[j] = UseSave;
}
}
}
return TargetArray;
}
冒泡排序的中心思想是:两两比较相邻记录的关键码,如果反序则交换它们的位置。
冒泡排序在进行一次循环后,一定有一个最大值被顺移到了最后面,就像一个泡泡浮起来那样,所以叫冒泡排序。
比如,我们有一个序列,里面最大值是88,这个我们一眼就能看出来,在冒泡中,我们进行一次J循环(即里面的那次for循环),就能确定地把这个最大值移到最后面去。原来的在最大值后面的序列,则依次往前顺移一位。一次冒泡排序的循环。
如下图:
我们使用冒泡排序输出第一次排序结果和我们想的是一样的:
对于冒泡排序,我们需要注意:
1.
由于第一次循环我们知道了最靠后的那个一定是最大的(就是88),所以我们第二次只需要对前面10个数中取出最大的就可以了。我们每次的循环依次都会选出最小的,所以J循环的遍历次数会依次变小,我们可以看到图中
这个地方要减去一个i,由于i循环每次都会加一,所以我们J循环每次减去i就可以保证我们不会遍历到我们后面已经摆放好的最大值。
2.
为什么两次循环都会设定循环次数不是n而是n-1呢,是因为:
- J循环中取不到n-1的原因,因为循环中要取得j+1的值,所以j最大只能取到n-2。如果J取到n-1,则array[J+1]等于array[n],array[n]是越界的。
- i循环中的n-1是为了减少循环次数,如果设定i<n,那么i最大会取到n-1,此时J会取到j<n-1-(n-1),那么,这里的条件就是j<0,显然不会成立则该for循环不会执行,那么让i能取到n-1显然没有意义,浪费循环。
插入排序
先帖代码:
public int[] InsertSort(int[] TargetArray,int n)
{
int i, j;
for (i = 1; i < n; i++)
{
int InsertValue = TargetArray[i];
for (j = i-1; j >= 0&& TargetArray[j] > InsertValue; j--)
{
TargetArray[j + 1] = TargetArray[j];
}
TargetArray[j + 1] = InsertValue;
}
return TargetArray;
}
插入排序的基本思想是依次将待排序的每个记录插入到一个排序好的序列当中。
通常设置数组的第一个数据是排序好的数据,剩余的为乱序的记录。然后将无序区的第一个记录插入到有序区中。
如果我们插入的值叫做X,我们从有序区的最大值开始比较,如果发现有一个有序区的值Y大于改无序区进入的值X,则将Y及其有序区后面的值依次往后挪一位,然后再将这个X值放在Y值的前面。
如图:
我们将16插入有序区,从有序区最后面开始比对,发现88和23都大于16,就将88、23往后挪一位,然后把16放在它们前面。有序区顺其自然扩充一位。
让代码每次插入都输出,我们也可以看到同样的结果:
对于插入排序的代码,我们需要注意:
1.
对于最外层的循环,是从1开始的,因为我们默认数组第一位已经是有序区中的值了,及array[0]就在有序区里。
2.
这个逻辑不能写在循环里,只能标注于for循环的表示中,如果我们将代码改成
那么,我们的循环将不管我们前面的值大于后面的值与否,都执行循环,导致J即使没有替换也无缘无故每次相减了,那么循环后面的TargetArray[j + 1]所代表的就一定是数组的第一位。就会错误。
3.
这个地方插入的值必须是J+1,因为上文在执行最后一次循环后,仍然执行了J--这个逻辑才进行循环判断,所以我们这个地方要把减去的值补回来,所以是J+1。
希尔排序
先上代码:
public int[] ShellSort2(int[] TargetArray, int n)
{
int i, j, Gap;
for (Gap = n / 2; Gap >= 1; Gap = Gap / 2)
{
for (i = Gap; i < n; i++)
{
for (j = i - Gap; j >= 0; j = j - Gap)
{
if (TargetArray[j] > TargetArray[j + Gap])
{
int Value = TargetArray[j + Gap];
TargetArray[j + Gap] = TargetArray[j];
TargetArray[j] = Value;
}
}
}
}
return TargetArray;
}
希尔排序是从插入排序进化而来的,希尔排序的基本思想是:将整个待排序记录分割成若干个对应的序列,然后让这些序列互相比较,若是反序则交换。
希尔排序最重要的是如何分割序列,一般说来,我们分割的值是数组长度/2。然后在每次循环中依次除以2来减小分割量。
例如我们的这个范例,第一次分割的Gap值就是11/2=5,序列中是第Gap位对应第0位,第Gap+1位对应第一位。放到我们的例子中就是0对应5。1对应6这样依次对应。对应好了然后互相比较,两个值谁小谁就放在前面。
例如我们第一次循环,那么会存在Gap等于5,第五位和第一位连在一起,进行比较。
让代码输出也是这样:
对于插希尔排序的代码,我们需要注意:
1.
虽然在Gap=n/2的时候只需要两两比对,但是第二次就会发生变化。
由于最里面的J循环会遍历,每次都会把J和J前面相隔Gap的值进行比对(第一次没有比对是因为Gap太大,所以J-Gap就小于0了),由于Gap值会不断除以2,每次J循环执行的次数会越来越多,相对的判断前后的次数也会越来越多。例如在Gap=2且J初始8时,会和它前面的6、4、2、0都进行比较,来达到后面较小的值(例如6)也可以一次循环快速前移的效果。
如图:
选择排序
先上代码:
public int[] ChooseSort2(int[] TargetArray,int n)
{
for (int i = 0; i < n; i++)
{
int SmallestIndex = i;
for (int j = i; j < n; j++)
{
if (TargetArray[SmallestIndex] > TargetArray[j])
{
SmallestIndex = j;
}
}
int Value = TargetArray[SmallestIndex];
TargetArray[SmallestIndex] = TargetArray[i];
TargetArray[i] = Value;
}
return TargetArray;
}
选择排序基本思想是,第i趟排序再待排序序列array[i]~array[n-1]中选择最小关键码的记录,并与第i个记录交换,作为有序序列的第i个记录。
对于一个选择排序序列来说,从0~n-1,首先选出最小的,和第一个置换,然后我们再在2~n-1中选出最小的,和2置换。道理很简单。只需要注意:
我们首先默认第i个就是最小的,然后和后面的进行比较,有比它大的直接修改索引就好了,不需要对数组进行修改。无论找到与否,都是要和i值做交换的。
四种排序性能对比
插入排序 | O(n^2) | 稳定 |
希尔排序 | O(n^2)~O(nLogn)之间(约为O(n^1.3)) | 不稳定 |
冒泡排序 | O(n^2) | 稳定 |
选择排序 | O(n^2) | 不稳定 |