一:排序的概述
排序可以看成是线性表的一种操作。排序是一个在现实生活中经常要遇到的问题,例如考试分数的排名(分数从大到小排列)、做操时的排队(身高从矮到高排列)
在计算机中的排序问题上,我们通常把数据元素称为记录
——排序的稳定性
当对a1,a2,a3三个待排记录进行降序排序,a1=a3,若排序后a1仍在a3前面,则称为稳定排序,若排序后a3排在a1前面则称为不稳定排序
——内排序与外排序
根据在排序过程中待排序的所有记录是否全部被放置在内存中,分为内排序和外排序
外排序是由于排序的记录个数太多,不能同时放置在内存中,整个排序过程需要在内外存之间进行多次数据交换才能进行
内排序是将排序的记录全部放置在内存中进行
一般只讨论内排序的一些方法,内排序分为插入排序、交换排序、选择排序和归并排序
二:最简单的排序算法
基本思想:将每个记录依次与它后面的记录进行比较,如果反序则交换
for (int i = 0; i < array.Length - 1; i++)
{
for (int j = i + 1; j < array.Length; j++)
{
if (array[j] < array[i])
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
外循环每趟循环完就能找到一个最小值,但是这个算法每趟循环的作用就只能找到一个最小值,对于其他位置并没有任何帮助,并且这个算法时间复杂度最好最坏情况下的次数都是O(n^2),也就是说,这个算法是非常低效的!
三:冒泡排序——交换排序
冒泡排序就像炒股,不断的买进卖出进行交换,想通过价格差来实现盈利
基本思想:比较相邻的两两记录,如果反序则交换,直到没有反序记录为止。共有两种思路——每次找到最大记录或每次找到最小记录
因为每趟排序都将最大或最小记录冒出来,因此叫作冒泡排序
//每次找到最小的记录
for (int i = 1; i <= array.Length - 1; i++)
{
for (int j = array.Length - 1; j >= i; j--)
{
if (array[j] < array[j - 1])
{
int temp = array[j];
array[j] = array[j - 1];
array[j - 1] = temp;
}
}
}
//每次找到最大的记录
for (int i = 1; i <= array.Length - 1; i++)
{
for (int j = 0; j < array.Length - i; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
外循环是趟数(n-1趟),内循环是本趟依次比较的两个记录的下标,每次找到最终的表尾元素或最终的表头记录
以上两种写法的时间复杂度最好最坏情况都是O(n^2),如果对于{2,1,3,4,5,6,7,8,9}的待排序列,第一趟交换完之后数列就已经有序了,但是算法仍然继续进行比较,很没有意义。当某一趟排序没有任何数据交换时,则说明序列已经有序了,所以对于冒泡排序有一种优化的写法
bool isSort = true;
for (int i = 1; i <= array.Length-1 && isSort; i++)
{
isSort = false;
for (int j = 0; j < array.Length - i; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
isSort = true;
}
}
}
优化之后最好情况下时间复杂度是O(n),最坏情况下时间复杂度是O(n^2)
四:简单选择排序——选择排序
简单选择排序就像炒股,很少出手,等到时机成熟再买进或卖出进行交换
基本思想:它不像冒泡排序一样进行多次的记录交换,而是每次找到一个最小的记录再进行交换
因为每趟排序并不是不断的交换记录,而是每趟遍历完之后选择一个最小的记录进行交换,因此叫作简单选择排序
int minIndex;
for (int i = 0; i < array.Length - 1; i++)
{
minIndex = i;
for (int j = i + 1; j < array.Length; j++)
{
if (array[j] < array[minIndex])
{
minIndex = j;
}
}
if (minIndex != i)
{
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
}
简单选择排序的最大特点就是交换记录的次数相当少,它的时间复杂度与冒泡排序相同,同为O(n^2),但是因为记录的交换次数少,所以在性能上还是略优于冒泡排序
五:直接插入排序——插入排序
直接插入排序就像平时玩牌时的摸牌,依次把小的插入到左边,大的插入到右边
基本思想:每趟都得到一串有序记录(第一趟有序记录就是下标为0位置的记录,它的下一个记录作为待排记录),每趟将待排记录插入到这串有序记录的适合位置,直到全部插入完为止
//i=1的写法(标准写法)
for (int i = 1; i < array.Length; i++)
{
if (array[i] < array[i - 1])
{
int temp = array[i];
int j;
for (j = i; j > 0 && array[j - 1] > temp; j--)
{
array[j] = array[j - 1];
}
array[j] = temp;
}
}
//i=0的写法
for (int i = 0; i < array.Length - 1; i++)
{
if (array[i + 1] < array[i])
{
int temp = array[i + 1];
int j;
for (j = i + 1; j > 0 && array[j - 1] > temp; j--)
{
array[j] = array[j - 1];
}
array[j] = temp;
}
}
直接插入排序算法在最好情况下的时间复杂度是O(n),最坏情况下的时间复杂度是O(n^2),它在性能上比冒泡排序和简单选择排序要好一些
引言:以上三种排序算法(冒泡排序,简单选择排序,直接插入排序)的时间复杂度都是都是O(n^2),下面几种排序算法都属于上面算法的改进版,让时间复杂度突破了O(n^2)的界限
六:希尔排序——插入排序
基本思想:按照增量进行分组,对每组记录采用直接插入排序的方法进行排序
int increment = array.Length;
while (increment > 1)
{
increment /= 2;
for (int i = 0; i < increment; i++)
{
for (int j = i + increment; j < array.Length; j += increment)
{
if (array[j] < array[j - increment])
{
int temp = array[j];
int k;
for (k = j - increment; k >= 0 && array[k] > temp; k -= increment)
{
array[k + increment] = array[k];
}
array[k + increment] = temp;
}
}
}
}
例如第六种排序算法直接插入排序例子中的最后一趟循环,2进行了7次交换才插入到了适合的位置,假设有100个记录,2同样排在最后一个,那么要进行99次交换才插入到适合的位置,那样的话岂不是很低效,这是因为直接插入排序每次只能够移动一个记录的位置,对后续的待排记录不会产生任何影响,而希尔排序可以进行跳跃式的移动,每次while循环后会将序列不断的排列为基本有序,当增量为1时,进行序列整体的直接插入排序
希尔排序的关键是增量值的选取,但是目前还没有人找到一种最好的增量序列,一般每次折半就好
希尔排序算法的时间复杂度是O(n^3/2),突破了O(n^2)的慢速排序,更加高效了,但是它是一种不稳定排序
七:堆排序——选择排序
堆排序不适合排序序列个数较少的情况,堆排序算法的时间复杂度是O(nlogn),但是它是一种不稳定排序
八:归并排序——归并排序
归并排序算法的时间复杂度是O(nlogn)
九:快速排序——交换排序
思想:分治法,通过一趟排序将待排记录分割成独立的两个部分,其中一部分的关键字均比另一部分记录的关键字小,再对这两部分继续进行递归排序,直到各部分中只有一个数
快速排序基本上都内置在了软件的开发工具包中,例如C#中的Array.Sort()底层实现就是用的快速排序算法
using System;
class Program
{
static int[] array = { 5, 6, 9, 8, 3, 7, 4, 1, 2 };
static void Main(string[] args)
{
Sort(0, 8);
}
//快速排序的递归调用函数
static void Sort(int low, int high)
{
if (low < high)
{
int pivot = Partition(low, high);
Sort(low, pivot - 1);
Sort(pivot + 1, high);
}
}
//将每次的待排序列分割为两部分
static int Partition(int low, int high)
{
//*****优化部分(三数取中)—使枢纽数pivot的选取更合理
int m = low + (high - low) / 2;
if (array[high] < array[low])
{
int temp = array[high];
array[high] = array[low];
array[low] = temp;
}
if (array[m] > array[high])
{
int temp = array[m];
array[m] = array[high];
array[high] = temp;
}
if (array[m] < array[low])
{
int temp = array[m];
array[m] = array[low];
array[low] = temp;
}
//*****
int key = array[low];
while (low < high)
{
while (low < high && array[high] >= key)
{
high--;
}
array[low] = array[high];
while (low < high && array[low] <= key)
{
low++;
}
array[high] = array[low];
}
array[low] = key;
return low;
}
}
以上代码中Partition函数尤为重要,就是每次选取不同的枢纽数pivot进行分割,不断的使每一段待排序列中的记录都满足小于pivot的数在pivot的左边,大于的在pivot的右边
快速排序不适合排序序列个数较少的情况,它是目前综合性能最佳的排序算法,快速排序算法的时间复杂度是O(nlogn),但是它是一种不稳定排序
十:总结
简单算法:冒泡排序、简单选择排序、直接插入排序
改进算法:快速排序、堆排序、希尔排序、归并排序
快速排序是性能最好的排序算法,但是对于不同的情况我们也应该考虑使用不用的算法应对