8种排序算法的C#实现
排序是将一个数据元素(或记录)的任意序列,重新排列成一个按关键字有序的序列。排序根据涉及的存储器的不同分为内部排序和外部排序:内部排序是指待排序记录存放在内存进行的排序过程;外部排序是指待排序记录的数量很大,以致内存一次不能容纳全部记录,在排序过程中尚需对外存进行访问的排序过程。本文仅讨论内部排序。
1.直接插入排序
最简单的排序方法,基本操作是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增1的有序表。
此处为了程序的通用,没有采取数组0位作为哨兵位的做法。
C#实现:
1 public static int[] StraightInsertionSort(int[] arr) 2 { 3 int temp; 4 for (int i = 1; i < arr.Length; i++) 5 { 6 if (arr[i] < arr[i - 1]) 7 { 8 //将arr[i]元素存入temp 9 temp = arr[i]; 10 int j = i; 11 12 //将i左侧比i大的元素右移一位 13 do 14 { 15 arr[j] = arr[j - 1]; 16 j--; 17 } while (j > 0 && temp < arr[j - 1]); 18 19 //将temp存入比i大的最小元素的左侧位置 20 arr[j] = temp; 21 } 22 23 /*上边实现方法的简化写法,但需要多判断一次,因为初始时j==i==1>0 24 int j = i; 25 while (j > 0 && arr[j] < arr[j - 1]) 26 { 27 temp = arr[j]; 28 arr[j] = arr[j - 1]; 29 arr[j - 1] = temp; 30 j--; 31 } 32 */ 33 } 34 return arr; 35 }
2.希尔排序
又称“缩小增量排序”,也是一种属插入排序类的方法,但在时间效率上较直接插入排序有较大的改进。
基本思想是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
C#实现:
1 public static int[] ShellSort(int[] arr) 2 { 3 int temp; 4 //初始步长 5 int gap = arr.Length / 2; 6 while (gap != 0) 7 { 8 #region 改进版的直接插入排序 9 for (int i = gap; i < arr.Length; i++) 10 { 11 if (arr[i] < arr[i - gap]) 12 { 13 temp = arr[i]; 14 int j = i; 15 16 do 17 { 18 arr[j] = arr[j - gap]; 19 j -= gap; 20 } while (j - gap >= 0 && temp < arr[j - gap]); 21 22 arr[j] = temp; 23 } 24 } 25 #endregion 26 27 //缩小增量 28 gap /= 2; 29 } 30 return arr; 31 }
3.冒泡排序
它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
C#实现:
1 public static int[] BubbleSort(int[] arr) 2 { 3 int temp; 4 //每趟排序将无序元素中的最小元素换到有序元素的右侧 5 for (int i = 0; i < arr.Length - 1; i++) 6 { 7 for (int j = arr.Length - 1; j > i; j--) 8 { 9 if (arr[j - 1] > arr[j]) 10 { 11 temp = arr[j - 1]; 12 arr[j - 1] = arr[j]; 13 arr[j] = temp; 14 } 15 } 16 } 17 return arr; 18 }
4.快速排序
对冒泡排序的一种改进,基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短。
C#实现:
1 public static int[] QuickSort(int[] arr) 2 { 3 return QuickSort(arr, 0, arr.Length - 1); 4 } 5 6 /// <summary> 7 /// 递归快速排序 8 /// </summary> 9 /// <param name="arr">目标数组</param> 10 /// <param name="low">数组起始位置</param> 11 /// <param name="high">数组终止位置</param> 12 /// <returns></returns> 13 private static int[] QuickSort(int[] arr, int low, int high) 14 { 15 if (low < high) 16 { 17 //将目标数组一分为二,得到枢轴位置 18 int pivotLoc = Partition(arr, low, high); 19 //对低子表递归排序 20 QuickSort(arr, low, pivotLoc - 1); 21 //对高子表递归排序 22 QuickSort(arr, pivotLoc + 1, high); 23 } 24 return arr; 25 } 26 27 private static void Swap(ref int index, ref int indexReplace) 28 { 29 int temp = index; 30 index = indexReplace; 31 indexReplace = temp; 32 } 33 34 /// <summary> 35 /// 一趟快排 36 /// </summary> 37 /// <param name="arr">目标数组</param> 38 /// <param name="low">数组起始位置</param> 39 /// <param name="high">数组终止位置</param> 40 /// <returns></returns> 41 private static int Partition(int[] arr, int low, int high) 42 { 43 //用子表的第一个记录作枢轴记录 44 int pivotKey = arr[low]; 45 //从表的两端交替地向中间扫描 46 while (low < high) 47 { 48 //将比枢轴记录小的记录交换到低端 49 while (low < high && arr[high] >= pivotKey) 50 { 51 --high; 52 } 53 arr[low] = arr[high]; 54 //将比枢轴记录大的记录交换到高端 55 while (low < high && arr[low] <= pivotKey) 56 { 57 ++low; 58 } 59 arr[high] = arr[low]; 60 } 61 //枢轴记录到位 62 arr[low] = pivotKey; 63 return low; 64 }
5.简单选择排序
一趟简单排序的操作为:通过n-i次关键字间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录交换。
C#实现:
1 public static int[] SimpleSelectionSort(int[] arr) 2 { 3 int temp; 4 //每趟排序将无序元素中的最小元素换到有序元素的右侧 5 for (int i = 0; i < arr.Length; i++) 6 { 7 int min = i; 8 for (int j = i + 1; j < arr.Length; j++) 9 { 10 if (arr[j] < arr[min]) 11 { 12 min = j; 13 } 14 } 15 temp = arr[i]; 16 arr[i] = arr[min]; 17 arr[min] = temp; 18 } 19 return arr; 20 }
6.堆排序
堆排序是一种树形选择排序,是对直接选择排序的有效改进。
初始时把要排序的n个数的序列看作是一棵顺序存储的二叉树(一维数组存储二叉树),调整它们的存储序,使之成为一个堆,将堆顶元素输出,得到n 个元素中最小(或最大)的元素,这时堆的根节点的数最小(或者最大)。然后对前面(n-1)个元素重新调整使之成为堆,输出堆顶元素,得到n 个元素中次小(或次大)的元素。依此类推,直到只有两个节点的堆,并对它们作交换,最后得到有n个节点的有序序列。
C#实现:
1 private static int heapSize; 2 public static int[] HeapSort(int[] arr) 3 { 4 heapSize = arr.Length; 5 //构建最大堆 6 for (int i = arr.Length / 2; i > 0; i--) 7 { 8 BuildMaxHeap(arr, i); 9 } 10 //将堆顶元素放入堆底的有序序列,并重新调整最大堆 11 for (int i = arr.Length - 1; i > 0; i--) 12 { 13 //每次在构建好最大堆后,将第一个元素arr[0]和最后一个元素arr[i]交换 14 swapHeap(arr, 0, i); 15 //此时arr[i]为堆的最大值,所以对arr[0]到arr[i - 1]元素进行调整 16 heapSize--; 17 //每次新组成的堆除了根节点其他的节点都保持最大堆的特性,因此只要再以根节点为基准调整就可以得到新的最大堆 18 BuildMaxHeap(arr, 1); 19 } 20 return arr; 21 } 22 23 //构建最大堆:调整当前节点和子节点使当前节点为最大元素 24 private static void BuildMaxHeap(int[] arr, int index) 25 { 26 int largeIndex = index; 27 //当前节点的左子节点 28 int leftChildIndex = index << 1; 29 //当前节点的右子节点 30 int rightChildIndex = (index << 1) + 1; 31 //左子节点存在且左子节点大于当前节点 32 if (leftChildIndex <= heapSize && arr[leftChildIndex - 1] > arr[largeIndex - 1]) 33 { 34 largeIndex = leftChildIndex; 35 } 36 //右子节点存在且右子节点大于当前节点 37 if (rightChildIndex <= heapSize && arr[rightChildIndex - 1] > arr[largeIndex - 1]) 38 { 39 largeIndex = rightChildIndex; 40 } 41 if (index != largeIndex) 42 { 43 swapHeap(arr, index - 1, largeIndex - 1); 44 BuildMaxHeap(arr, largeIndex); 45 } 46 } 47 48 //互换堆中的两个值 49 private static void swapHeap(int[] arr, int index, int indexReplace) 50 { 51 int temp = arr[index]; 52 arr[index] = arr[indexReplace]; 53 arr[indexReplace] = temp; 54 }
7.归并排序
C#实现:
1 public static int[] MergeSort(int[] arr) 2 { 3 //在整个排序过程中始终使用同一个暂存数组,空间利用率高 4 int[] tempArr = new int[arr.Length]; 5 return MergeSort(arr, tempArr, 0, arr.Length - 1); 6 } 7 8 /// <summary> 9 /// 将目标数组循环折半,递归执行归并排序核心,再组合成有序数组 10 /// </summary> 11 /// <param name="arr">目标数组</param> 12 /// <param name="tempArr">暂存数组</param> 13 /// <param name="first">子表的起始位置</param> 14 /// <param name="last">子表的终止位置</param> 15 /// <returns></returns> 16 private static int[] MergeSort(int[] arr, int[] tempArr, int first, int last) 17 { 18 if (first < last) 19 { 20 int mid = (first + last) / 2; 21 MergeSort(arr, tempArr, first, mid); 22 MergeSort(arr, tempArr, mid + 1, last); 23 MergeSortCore(arr, tempArr, first, mid, last); 24 } 25 return arr; 26 } 27 28 /// <summary> 29 /// 归并排序核心:将两个有序的左右子表(以mid区分),合并成一个有序表 30 /// </summary> 31 /// <param name="arr">目标数组</param> 32 /// <param name="tempArr">暂存数组</param> 33 /// <param name="first">子表的起始位置</param> 34 /// <param name="mid">子表的划分位置</param> 35 /// <param name="last">子表的终止位置</param> 36 /// <returns></returns> 37 private static int[] MergeSortCore(int[] arr, int[] tempArr, int first, int mid, int last) 38 { 39 //左侧子表的起始位置 40 int indexA = first; 41 //右侧子表的起始位置 42 int indexB = mid + 1; 43 int tempIndex = 0; 44 //遍历左右子表,直到其中一个表遍历完 45 while (indexA <= mid && indexB <= last) 46 { 47 //左子表的最小元素 <= 右子表的最小元素 48 if (arr[indexA] <= arr[indexB]) 49 { 50 //左子表的最小元素暂存数组,遍历左子表下标+1 51 tempArr[tempIndex++] = arr[indexA++]; 52 } 53 else 54 { 55 tempArr[tempIndex++] = arr[indexB++]; 56 } 57 } 58 //有一侧子表遍历完后,将另一侧子表剩下的数一次放入暂存数组中,暂存数组保持有序 59 while (indexA <= mid) 60 { 61 tempArr[tempIndex++] = arr[indexA++]; 62 } 63 while (indexB <= last) 64 { 65 tempArr[tempIndex++] = arr[indexB++]; 66 } 67 //将暂存数组中有序的元素写回到目标数组的指定位置,使进行归并的数组段有序 68 tempIndex = 0; 69 for (int i = first; i <= last; i++) 70 { 71 arr[i] = tempArr[tempIndex++]; 72 } 73 return arr; 74 }
8.基数排序
基数排序是借助“分配”和“收集”两种操作对单逻辑关键字进行排序的一种内部排序方法。例如,若关键字是数值,且其值都在0<=K<=999范围内,则可把每一个十进制数字看成一个关键字,即可认为K由3个关键字(Kº,K¹,K²)组成,其中Kº是百位数,K¹是十位数,K²是个位数。
C#实现:
1 public static int[] RadixSort(int[] arr) 2 { 3 //数组中数字的最大位数 4 int maxDigit = 0; 5 6 //查找最大位数 7 foreach (int item in arr) 8 { 9 int tempDigit = item.ToString().Length; 10 if (tempDigit > maxDigit) 11 { 12 maxDigit = tempDigit; 13 } 14 } 15 16 for (int i = 0; i < maxDigit; i++) 17 { 18 int[] tempArr = new int[arr.Length]; 19 int[] countArr = new int[10]; 20 21 for (int j = 0; j < arr.Length; j++) 22 { 23 //数组中每个元素在i位上的数字 = 该元素在<=i位的数字 - 该元素在<=(i-1)位的元素 24 int splitNum = (int)(arr[j] / Math.Pow(10, i)) - (int)(arr[j] / Math.Pow(10, i + 1)) * 10; 25 //累加每个元素在i位上的数字出现的次数,如12,118,92三个数字组成的数组,则当i==1(每个数字的十位)时,countArr[1] == 2, countArr[9] == 1,其余为0 26 countArr[splitNum]++; 27 } 28 29 //累加在i位上出现j及比j小的数字的次数 30 for (int j = 1; j < 10; j++) 31 { 32 countArr[j] += countArr[j - 1]; 33 } 34 35 //从后向前遍历数组,根据每个元素i位上的数字(splitNum),得到比该数字小或等于该数字的元素个数(countArr[splitNum]),即为该元素在排序后数组中的位置(splitNumIndex) 36 for (int j = arr.Length - 1; j >= 0; j--) 37 { 38 int splitNum = (int)(arr[j] / Math.Pow(10, i)) - (int)(arr[j] / Math.Pow(10, i + 1)) * 10; 39 int splitNumIndex = countArr[splitNum] - 1; 40 tempArr[splitNumIndex] = arr[j]; 41 42 countArr[splitNum]--; 43 } 44 Array.Copy(tempArr, arr, tempArr.Length); 45 } 46 return arr; 47 }
总结:各排序算法的稳定性,时间、空间复杂度比较
附:上述代码的调用示例
1 static void Main(string[] args) 2 { 3 int[] arr = { 1, 51, 15, 15, 4, 18, 12, 54, 3, 53, 9, 58, 0, 26, 325, 7, 5, 56, 842, 785, 93, 65, 10, 87, 8, 66, 2, 12, 678, 35, 33, 32, 62, 624, 63, 6, 25, 8, 11, 89, 46 }; 4 int[] arrResult = Sort.RadixSort(arr); 5 for (int i = 0; i < arrResult.Length; i++) 6 { 7 Console.Write(arrResult[i]); 8 if (i < arrResult.Length - 1) 9 { 10 Console.Write(","); 11 } 12 } 13 Console.Read(); 14 }
参考资料:八大排序算法 数据结构(C语言版)