数据结构与算法(八)——排序
一、排序
1、介绍
影响排序算法性能的几个要素:时间性能、辅助空间、算法的复杂性。
内部排序:将需要处理的所有数据都加载到内存中进行排序。包括交换式排序、选择式排序、插入式排序。
外部排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括合并排序、直接合并排序。
2、分类
交换式排序:冒泡排序(Bubble sort)、快速排序(Quick sort)
选择式排序:选择排序(Select sort)、堆排序(Heap sort)
插入式排序:插入排序(Insert sort)、希尔排序(Shell sort)、二叉树排序(Binary-tree sort)
归并式排序:归并排序()
二、冒泡排序
1、思想
两个数比较大小,较大的数下沉,较小的数冒起来。每一趟至少有一个数排好,所以 n 个数至多需要 n - 1 趟。
2、过程
动图:
3、代码
1 // 冒泡排序 2 public static void bubbleSort(int[] arr) { 3 // 冒泡排序一趟至少可以确定一个数的位置 4 boolean flag = false; 5 for (int i = 0; i < arr.length - 1; i++) { 6 flag = false; 7 for (int j = 0; j < arr.length - 1 - i; j++) { 8 if (arr[j] > arr[j + 1]) { 9 swap(arr, j, j + 1); 10 flag = true; 11 } 12 } 13 // 没有执行交换,已经有序 14 if (!flag) { 15 break; 16 } 17 } 18 }
三、快速排序
1、思想
快速排序:是对冒泡排序的一种改进。
(1)在数据集之中,选择一个元素作为"基准"(pivot)。
(2)所有小于"基准"的元素,都移到"基准"的左边;所有大于"基准"的元素,都移到"基准"的右边。
(3)对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。整个排序过程可以递归进行,以此达到整个数据有序。
2、过程
3、代码
1 // 快速排序 2 public static void quickSort(int[] arr, int left, int right) { 3 if (left >= right) { 4 return; 5 } 6 int l = left; 7 int r = right; 8 int pivot = arr[r]; 9 10 while (l < r) { 11 while (l < r && arr[l] < pivot) { 12 l++; 13 } 14 if (l < r) { 15 arr[r] = arr[l]; 16 r--; 17 } 18 19 while (l < r && arr[r] > pivot) { 20 r--; 21 } 22 if (l < r) { 23 arr[l] = arr[r]; 24 l++; 25 } 26 } 27 // 此时 l == r 28 arr[l] = pivot; 29 quickSort(arr, left, r - 1); 30 quickSort(arr, l + 1, right); 31 }
四、选择排序
1、思想
在长度为 n 的无序数组中,
第一次遍历 n 个数,找到最小的数值与第一个元素交换;
第二次遍历n - 1个数,找到最小的数值与第二个元素交换;
。。。
第n - 1次遍历2个数,找到最小的数值与第n - 1个元素交换,排序完成。
2、过程
动图:
3、代码
1 // 选择排序.方式一、少交换 2 public static void selectSort(int[] arr) { 3 int min = 0, index = 0; 4 for (int i = 0; i < arr.length - 1; i++) { 5 min = arr[i]; 6 index = i; 7 // 找最小值 8 for (int j = i + 1; j < arr.length; j++) { 9 if (arr[j] < min) { 10 min = arr[j]; 11 index = j; 12 } 13 } 14 // 将最小值与第 i 位交换 15 swap(arr, i, index); 16 } 17 } 18 19 // 选择排序.方式二、多交换 20 public static void selectSort2(int[] arr) { 21 for (int i = 0; i < arr.length - 1; i++) { 22 for (int j = i + 1; j < arr.length; j++) { 23 if (arr[i] > arr[j]) { 24 swap(arr, i, j); 25 } 26 } 27 } 28 }
五、插入排序
1、思想
将一个数插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。
2、过程
动图:
3、代码
1 // 插入排序 2 public static void insertSort(int[] arr) { 3 for (int i = 1; i < arr.length; i++) { 4 // 待插入数 5 int insertVal = arr[i]; 6 int index = i - 1; 7 while (index >= 0 && insertVal < arr[index]) { 8 arr[index + 1] = arr[index]; 9 index--; 10 } 11 arr[index + 1] = insertVal; 12 } 13 }
六、希尔排序
插入排序可能存在的问题,数组 arr = {2,3,4,5,6,1} 这时需要插入数 1,这样的过程是:
{2,3,4,5,6,6}
{2,3,4,5,5,6}
{2,3,4,4,5,6}
{2,3,3,4,5,6}
{2,2,3,4,5,6}
{1,2,3,4,5,6}
结论:当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响。
1、思想
希尔排序:是对插入排序的一种改进。也称为缩小增量排序。
在要排序的一组数中,根据某一增量分为若干子序列,并对子序列分别进行插入排序。然后逐渐将增量减小,并重复上述过程,直至增量为1,此时数据序列基本有序,最后进行插入排序。
2、过程
动图:
3、代码
1 // 希尔排序.方式一、移位法 2 public static void shellSort(int[] arr) { 3 // 增量 gap,并逐步缩小增量 4 for (int gap = arr.length / 2; gap > 0; gap = gap / 2) { 5 // 从第gap个元素,逐个对其所在的组进行直接插入排序 6 for (int i = gap; i < arr.length; i++) { 7 // 待插入数 8 int insertVal = arr[i]; 9 int index = i - gap; 10 while (index >= 0 && insertVal < arr[index]) { 11 arr[index + gap] = arr[index]; 12 index = index - gap; 13 } 14 arr[index + gap] = insertVal; 15 } 16 } 17 } 18 19 // 希尔排序.方式二、交换法 20 public static void shellSort2(int[] arr) { 21 // 分组.个人感觉这种方式组内排序更像是冒泡 22 for (int gap = arr.length / 2; gap > 0; gap = gap / 2) { 23 for (int i = gap; i < arr.length; i++) { 24 25 for (int j = i - gap; j >= 0; j = j - gap) { 26 if (arr[j] > arr[j + gap]) { 27 swap(arr, j, j + gap); 28 } 29 } 30 } 31 } 32 }
七、归并排序
1、思想
该算法采用经典的分治(divide-and-conquer)策略。分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。
2、过程
分阶段:可以理解为就是递归拆分子序列的过程。
治阶段:将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8]。
动图:
3、代码
1 // 归并排序.分+合方法 2 public static void mergeSort(int[] arr, int left, int right, int[] temp) { 3 if (left < right) { 4 int mid = (left + right) / 2; 5 // 向左递归进行分解 6 mergeSort(arr, left, mid, temp); 7 // 向右递归进行分解 8 mergeSort(arr, mid + 1, right, temp); 9 // 合并 10 merge(arr, left, mid, right, temp); 11 } 12 } 13 14 // 合并的方法 15 private static void merge(int[] arr, int left, int mid, int right, int[] temp) { 16 int i = left; 17 int j = mid + 1; 18 int tempIndex = 0; 19 20 // 1.合并两个有序序列到temp 21 while (i <= mid && j <= right) { 22 if (arr[i] <= arr[j]) { 23 temp[tempIndex] = arr[i]; 24 i++; 25 } else { 26 temp[tempIndex] = arr[j]; 27 j++; 28 } 29 tempIndex++; 30 31 } 32 33 while (i <= mid) { 34 temp[tempIndex] = arr[i]; 35 i++; 36 tempIndex++; 37 } 38 while (j <= right) { 39 temp[tempIndex] = arr[j]; 40 j++; 41 tempIndex++; 42 } 43 44 // 2.将temp数组的元素拷贝到arr 45 tempIndex = 0; 46 int tempLeft = left; 47 48 while (tempLeft <= right) { 49 arr[tempLeft] = temp[tempIndex]; 50 tempLeft++; 51 tempIndex++; 52 } 53 }
八、基数排序
1、思想
基数排序(radix sort)属于"分配式排序"(distribution sort),又称"桶子法"(bucket sort)。顾名思义,将关键字按位数切割成不同的数字,然后按每个位数分别比较,这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
2、过程
将数组{53,3,542,748,14,214}使用基数排序,进行升序排序。
动图:
说明:
基数排序是对传统桶排序的扩展,速度很快。
基数排序是经典的空间换时间的方式,占用内存很大。当对海量数据排序时,容易造成 OutOfMemoryError。
有负数的数组,我们不用基数排序来进行排序。如果要支持负数,参考:https://code.i-harness.com/zh-CN/q/e98fa9
3、代码
1 // 基数排序 2 public static void radixSort(int[] arr) { 3 // 定义桶的个数.0~9 一共10个 4 final int bucketCount = 10; 5 6 // 由于每一个桶都是一个一维数组,所以不难想到定义一个二维数组来表示 7 int[][] buckets = new int[bucketCount][arr.length]; 8 9 // 为每一个桶定义一个指针,表示当前桶中实际放入的元素个数 10 int[] indexs = new int[bucketCount]; 11 12 final int maxValueLength = getMaxValueLength(arr); 13 for (int i = 0; i < maxValueLength; i++) { 14 15 // 1.arr -> 放入桶 16 for (int j : arr) { 17 // 依次取个位、十位、百位……上的数字.比如取十位: 748/10=74,74%10=4 18 int digit = j / (int) Math.pow(10, i) % 10; 19 20 // 放入到对应的桶中 21 buckets[digit][indexs[digit]] = j; 22 // 对应桶的个数 +1 23 indexs[digit]++; 24 } 25 26 // 2.从桶取 -> arr 27 int temp = 0; 28 // 遍历这 10 个桶 29 for (int j = 0; j < bucketCount; j++) { 30 if (indexs[j] > 0) { 31 for (int k = 0; k < indexs[j]; k++) { 32 arr[temp] = buckets[j][k]; 33 temp++; 34 } 35 } 36 37 // 取完 j 号桶后,将指针归零 38 indexs[j] = 0; 39 } 40 } 41 } 42 43 // 获取最大数是几位数 44 private static int getMaxValueLength(int[] arr) { 45 int max = arr[0]; 46 for (int i : arr) { 47 if (i > max) { 48 max = i; 49 } 50 } 51 52 return String.valueOf(max).length(); 53 }
九、常用排序算法对比
相关术语解释:
1)稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
2)不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
3)内排序:所有排序操作都在内存中完成;
4)外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
5)时间复杂度: 一个算法执行所耗费的时间。
6)空间复杂度:运行完一个程序所需内存的大小。
7)n:数据规模
8)k:"桶"的个数
9)In-place:不占用额外内存
10)Out-place:占用额外内存
作者:Craftsman-L
本博客所有文章仅用于学习、研究和交流目的,版权归作者所有,欢迎非商业性质转载。
如果本篇博客给您带来帮助,请作者喝杯咖啡吧!点击下面打赏,您的支持是我最大的动力!