排 序
1.文件:由一组记录组成,记录有若干数据项组成,唯一标识记录的数据项称关键字;
2.排序是将文件按关键字的递增(减)顺序排列;
3.排序文件中有相同的关键字时,若排序后相对次序保持不变的称稳定排序,否则称不稳定排序;
4.在排序过程中,文件放在内存中处理不涉及数据的内、外存交换的称内排序,反之称外排序;
5.排序算法的基本操作:1)比较关键字的大小;2)改变指向记录的指针或移动记录本身。
6.评价排序方法的标准:1)执行时间;2)所需辅助空间,辅助空间为O(1)称就地排序;另要注意算法的复杂程度。
7.若关键字类型没有比较运算符,可事先定义宏或函数表示比较运算。
8.插入排序
(1)直接插入排序
算法中引入监视哨R[0]的作用是:1)保存R[i]的副本;2)简化边界条件,防止循环下标越界。
关键字比较次数最大为(n+2)(n-1)/2;记录移动次数最大为(n+4)(n-1)/2;
算法的最好时间是O(n);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的稳定的排序;
(2)希尔排序
实现过程:是将直接插入排序的间隔变为d。d的取值要注意:1)最后一次必为1;2)避免d值互为倍数;
关键字比较次数最大为n^1.25;记录移动次数最大为1.6n^1.25;
算法的平均时间是O(n^1.25);是一种就地的不稳定的排序;
9.交换排序
(1)冒泡排序
实现过程:从下到上相邻两个比较,按小在上原则扫描一次,确定最小值,重复n-1次。
关键字比较次数最小为n-1、最大为n(n-1)/2;记录移动次数最小为0,最大为3n(n-1)/2;
算法的最好时间是O(n);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的稳定的排序;
(2)快速排序
实现过程:将第一个值作为基准,设置i,j指针交替从两头与基准比较,有交换后,交换j,i。i=j时确定基准,并以其为界限将序列分为两段。重复以上步骤。
关键字比较次数最好为nlog2n+nC(1)、最坏为n(n-1)/2;
算法的最好时间是O(nlog2n);最坏时间是O(n^2);平均时间是O(nlog2n);辅助空间为O(log2n);是一种不稳定排序;
10.选择排序
(1)直接选择排序
实现过程:选择序列中最小的插入第一位,在剩余的序列中重复上一步,共重复n-1次。
关键字比较次数为n(n-1)/2;记录移动次数最小为0,最大为3(n-1);
算法的最好时间是O(n^2);最坏时间是O(n^2);平均时间是O(n^2);是一种就地的不稳定的排序;
(2)堆排序
实现过程:把序列按层次填入完全二叉树,调整位置使双亲大于或小于孩子,建立初始大根或小根堆,调整树根与最后一个叶子的位置,排除该叶子重新调整位置。
算法的最好时间是O(nlog2n);最坏时间是O(nlog2n);平均时间是O(nlog2n);是一种就地的不稳定排序;
11.归并排序
实现过程:将初始序列分为2个一组,最后单数轮空,对每一组排序后作为一个单元,对2个单元排序,直到结束。
算法的最好时间是O(nlog2n);最坏时间是O(nlog2n);平均时间是O(nlog2n);辅助空间为O(n);是一种稳定排序;
12.分配排序
(1)箱排序
实现过程:按关键字的取值范围确定箱子的个数,将序列按关键字放入箱中,输出非空箱的关键字。
在桶内分配和收集,及对各桶进行插入排序的时间为O(n),算法的期望时间是O(n),最坏时间是O(n^2)。
(2)基数排序
实现过程:按基数设置箱子,对关键字从低位到高位依次进行箱排序。
算法的最好时间是O(d*n+d*rd);最坏时间是O(d*n+d*rd);平均时间是O(d*n+d*rd);辅助空间O(n+rd);是一种稳定排序;
// 这意味着,大的元素总是在向后慢慢移动直到遇到比它更大的元素。所以每一轮交换完成都能将最大值冒到最后)
public static void BubbleSort(IList<int> data)
{
for (int i = data.Count - 1; i > 0; i--)
{
for (int j = 0; j < i; j++)
{
if (data[j] > data[j + 1])
Swap(data, j, j + 1);
}
}
}
private static void OrderBy(int[] arr)
{
for (int r = 0; r < arr.Length - 1; r++)//0
{
int minIndex = r;
for (int c = r + 1; c < arr.Length; c++)//1 2 3 4
{
if (arr[minIndex] > arr[c])
{
//记录最小索引
minIndex = c;
}
}
if (minIndex != r)
{
//交换
int temp = arr[r];
arr[r] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
//算法思路:
//⒈ 从第一个元素开始,该元素可以认为已经被排序
//⒉ 取出下一个元素,在已经排序的元素序列中从后向前扫描
//⒊ 如果该元素(已排序)大于新元素,将该元素移到下一位置
//⒋ 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
//⒌ 将新元素插入到下一位置中
//⒍ 重复步骤2~5
private static void InsertSort(int[] arr)
{
//插入排序是把无序列的数一个一个插入到有序的数
//先默认下标为0这个数已经是有序
for (int i = 1; i < arr.Length; i++)
{
int insertVal = arr[i]; //首先记住这个预备要插入的数
int insertIndex = i - 1; //找出它前一个数的下标(等下 准备插入的数 要跟这个数做比较)
while (insertIndex >= 0 && insertVal < arr[insertIndex]) //这里小于是升序,大于是降序
{
arr[insertIndex + 1] = arr[insertIndex]; //同时把比插入数要大的数往后移
insertIndex--; //指针继续往后移,等下插入的数也要跟这个指针指向的数做比较
}
//插入(这时候给insertVal找到适当位置)
arr[insertIndex + 1] = insertVal;
}
}
// 最小数列,每次都让分割出的数列符合“哨兵”的规则,自然就将数列变得有序)
//具体过程:
//设序列为R[low,high],从其中选第一个为基准,设为keyValue,然后设两个指针i和j,分别指向序列R[low,high]的起始和结束位置上:
// 1),将i逐渐增大,直到找到大于keyValue的关键字为止;
// 2),将j逐渐减少,直到找到小于等于keyValue的关键字为止;
// 3),如果i<j,即R[i,j]的元素数大于1,则交换R[i]和R[j];
// 4),将基准记录keyValue放到合适的位置上,即i和j同时指向的位置(或者同时指向的位置-1),则此位置为新的keyValuePosition。
public static void QuickSortStrict(IList<int> data, int low, int high)
{
if (low >= high) return;
int temp = data[low];
int i = low + 1, j = high;
while (true)
{
while (data[j] > temp) j--;
while (data[i] < temp && i < j) i++;
if (i >= j) break;
Swap(data, i, j);
i++; j--;
}
if (j != low)
Swap(data, low, j);
QuickSortStrict(data, j + 1, high);
QuickSortStrict(data, low, j - 1);
}
{
var temp = data[i];
data[i] = data[j];
data[j] = temp;
}
//过程解析:将数列分为两部分,分别得到两部分数列的有序版本,然后逐个比较,将比较出的小数逐个放进
// 新的空数列中。当一个数列放完后,将另一个数列剩余数全部放进去
public static IList<int> MergeSort(IList<int> data, int low, int high)
{
int length = high - low + 1;
IList<int> mergeData = new int[length];
if (low == high)
{
mergeData[0] = data[low];
return mergeData;
}
int mid = (low + high) / 2;
IList<int> leftData = MergeSort(data, low, mid);
IList<int> rightData = MergeSort(data, mid + 1, high);
int i = 0, j = 0;
while (true)
{
if (leftData[i] < rightData[j])
{
mergeData[i + j] = leftData[i++]; //不能使用Add,Array Length不可变
if (i == leftData.Count)
{
int rightLeft = rightData.Count - j;
for (int m = 0; m < rightLeft; m++)
{
mergeData[i + j] = rightData[j++];
}
break;
}
}
else
{
mergeData[i + j] = rightData[j++];
if (j == rightData.Count)
{
int leftleft = leftData.Count - i;
for (int n = 0; n < leftleft; n++)
{
mergeData[i + j] = leftData[i++];
}
break;
}
}
}
return mergeData;
}
//原理:将数列构建为最大堆数列(即父节点总是最大值),将最大值(即根节点)交换到数列末尾
// 这样要排序的数列数总和减少,同时根节点不再是最大值,调整最大堆数列。如此重复,最后得到有序数列
//实现准备:如何将数列构造为堆——父节点i的左子节点为2i+1,右子节点为2i+2。节点i的父节点为floor((i-1)/2)
/// <summary>
/// 堆排序方法。
/// </summary>
/// <param name="a">
/// 待排序数组。
/// </param>
private static void HeapSort(int[] a)
{
BuildMaxHeap(a); // 建立大根堆。
for (int i = a.Length - 1; i > 0; i--)
{
Swap(a,0,i); // 将堆顶元素和无序区的最后一个元素交换。
MaxHeaping(a, 0, i); // 将新的无序区调整为大根堆。
for (int j = 0; j < i; j++)
{
Console.Write(a[j] + " ");
}
}
}
/// 由底向上建堆。由完全二叉树的性质可知,叶子结点是从index=a.Length/2开始,
/// 所以从index=(a.Length/2)-1结点开始由底向上进行大根堆的调整。
/// </summary>
/// <param name="a">
/// 待排序数组。
/// </param>
private static void BuildMaxHeap(int[] a)
{
for (int i = (a.Length / 2) - 1; i >= 0; i--)
{
MaxHeaping(a, i, a.Length);
}
}
/// 将指定的结点调整为堆。
/// </summary>
/// <param name="a">
/// 待排序数组。
/// </param>
/// <param name="i">
/// 需要调整的结点。
/// </param>
/// <param name="heapSize">
/// 堆的大小,也指数组中无序区的长度。
/// </param>
private static void MaxHeaping(int[] a, int i, int heapSize)
{
int left = (2 * i) + 1; // 左子结点。
int right = 2 * (i + 1); // 右子结点。
int large = i; // 临时变量,存放大的结点值。
if (left < heapSize && a[left] > a[large])
{
large = left;
}
if (right < heapSize && a[right] > a[large])
{
large = right;
}
if (i != large)
{
Swap(a,i,large);
MaxHeaping(a, large, heapSize);
}
}
//在前面介绍的插入排序,我们知道
//1.它对有序数列排序的效率是非常高的
//2.要排序的数向前移动是一步步进行的导致插入排序效率低。
//希尔排序正是利用第一点,改善第二点,达到更理想的效果)
//原理:通过奇妙的步长,插入排序间隔步长的元素,随后逐渐缩短步长至1,实现数列的插入排序
//过程解析:采用的步长是N/2,每次取半,直至1。循环内部就是标准的插入排序
public static void ShellSort(IList<int> data)
{
int temp;
for (int gap = data.Count / 2; gap > 0; gap /= 2)
{
for (int i = gap; i < data.Count; i += gap)
{
temp = data[i];
for (int j = i - gap; j >= 0; j -= gap)
{
if (data[j] > temp)
{
data[j + gap] = data[j];
if (j == 0)
{
data[j] = temp;
break;
}
}
else
{
data[j + gap] = temp;
break;
}
}
}
}
}
13.各种内部排序方法的比较和选择:
(1)按平均时间复杂度分为:
1) 平方阶排序:直接插入、直接选择、冒泡排序;
2) 线性对数阶:快速排序、堆排序、归并排序;
3) 指数阶:希尔排序;
4) 线性阶:箱排序、基数排序。
(2)选择合适排序方法的因素:
1)待排序的记录数;2)记录的大小;3)关键字的结构和初始状态;4)对稳定性的要求;
5)语言工具的条件;6)存储结构; 7)时间和辅助空间复杂度。
(3)结论:
1) 若规模较小可采用直接插入或直接选择排序;
2) 若文件初始状态基本有序可采用直接插入、冒泡或随机快速排序;
3) 若规模较大可采用快速排序、堆排序或归并排序;
4) 任何借助于比较的排序,至少需要O(nlog2n)的时间,箱排序和基数排序只适用于有明显结构特征的关键字;
5) 有的语言没有提供指针及递归,使归并、快速、基数排序算法复杂;
6) 记录规模较大时为避免大量移动记录可用链表作为存储结构,如插入、归并、基数排序,但快速、堆排序在链表上难以实现,可提取关键字建立索引表,然后对索引表排序。