数据结构8---排序
1 排序的概念及引用
1.1 排序的概念
所谓排序,就是使一串记录按照一定的顺序排列起来
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳 定的;否则称为不稳定的。
1.2 常见的排序算法
2 常见排序算法的实现
2.1 插入排序
2.1.1直接插入排序
就像理扑克牌一样,不断将新元素拿到已排序的队伍中,帮它找到正确的位置插进去。
public static void insertSort(int[] array){int tmp = 0;for (int i = 1; i <array.length ; i++) {tmp = array[i];int j = i-1;for(;j>=0;j--){if(array[j] > tmp){array[j+1] = array[j];}else{break;}}array[j+1] = tmp;}}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
2.1.2希尔排序
先选定一个整数,把待排序文件中所有记录分成多个组, 所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达 =1时,所有记录在统一组内排好序。
public static void shellSort(int[] array){int gap = array.length;while(gap > 0){gap /= 2;shell(array,gap);}}public static void shell(int[] array,int gap){int tmp = 0;for (int i = gap; i <array.length ; i++) {tmp = array[i];int j = i-gap;for(;j>=0;j-=gap){if(array[j] > tmp){array[j+gap] = array[j];}else{break;}}array[j+gap] = tmp;}}
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很 快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排 序的时间复杂度都不固定
4. 稳定性:不稳定
2.2 选择排序
2.2.1 直接选择排序
选择排序就是不断地从还没排好的数据中“选择”出一个最小的(或最大的),然后放到已经排好的数据的末尾。
它的核心就两步,不断循环:
找最小:遍历未排序的部分,找到最小的元素。
交换位置:把这个最小的元素和未排序部分的第一个元素交换位置。
public static void selectSort(int[] array){for (int i = 0; i < array.length ; i++) {int minIndext = i;for (int j = i+1; j <array.length ; j++) {if(array[j] < array[minIndext]){minIndext = j;}}int tmp = array[i];array[i] = array[minIndext];array[minIndext] = tmp;}}
2.2.2堆排序
通过堆这种数据结构来排序
public static void heapSort(int[] array){//升序建大堆creatBigHeap(array);//将第一个与最后一个一一交换,并向下调整为新的大堆int end = array.length-1;while(end > 0){swap(array,0,end);shiftDown(array,0,end);end--;}}private static void creatBigHeap(int[] array){for (int i = (array.length-2)/2; i >=0 ; i--) {shiftDown(array,i,array.length);}}private static void shiftDown(int[] array ,int parent,int end){int child = 2*parent+1;while(child < end){if(child+1<end && array[child] < array[child+1]){child++;}if (array[child] > array[parent]) {swap(array,child,parent);parent = child;child = 2*child+1;}else{break;}}}private static void swap(int[] array,int i,int j){int tmp = array[i];array[i] = array[j];array[j] = tmp;}
直接排序特性总结:
1. 堆排序使用堆来选数,效率就高了很多。 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(1) 4. 稳定性:不稳定
2.3 交换排序
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特 点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
2.3.1 冒泡排序
public static void bubbleSort(int[] array){for (int i = 0; i < array.length-1; i++) {boolean flg = true; for (int j = 0; j < array.length-1-i; j++) {if(array[j] > array[j+1]){swap(array,j,j+1);flg = false; }}if(flg){return;}}}
冒泡排序特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定
2.3.2快速排序
快速排序是一种高效的排序算法,采用“分治法”策略。其核心操作是“分区”,具体步骤如下:
选择基准值:从数组中选择一个元素作为“基准值”。这个选择可以是随机的,比如第一个元素、最后一个元素或中间元素。
分区操作:重新排列数组,使得所有比基准值小的元素都移到基准值的左边,所有比基准值大的元素都移到基准值的右边。这个操作结束后,基准值就处于它最终的正确位置上。
递归排序:递归地将小于基准值的子数组和大于基准值的子数组进行快速排序。
递归的“底部”情况是:当子数组的大小为零或一时,它已经被排序,不需要再进行操作。
关键点在于分区操作,它使得每次递归调用都能至少将一个元素(基准值)放到正确的位置上。快速排序的平均时间复杂度为 O(n log n),是最快的通用排序算法之一。不过,在最坏情况下(比如数组已经有序),其时间复杂度会退化到 O(n²)。
1 Hoare版
public static void quickSort(int[] array){quick(array,0,array.length-1);}private static void quick(int[] array, int start, int end) {//结束的标志 只有一个节点或者一个节点都没有if(start >= end) return ;//求基准int pivot = parttion(array,start,end);//递归左边quick(array,start,pivot-1);//递归右边quick(array,pivot+1,end);}private static int parttion(int[] array, int left, int right) {int key = array[left];int i = left;while(left < right){while(left<right && array[right]>= key){//防止right越界right--;}//right下标一定是比key小的数据while(left < right && array[left] <= key){left++;}//left 下标一定是比key大的数据swap(array,left,right);}//此时left与right相遇 ,相遇的位置和i位置数值进行交换swap(array,left,i);return left;}
注意:
①.为什么parttion方法中的第二个while条件要取等号?如果不取等号,遇到最左边和最右边都是一样的数字的话,那么right和left都不会动,就会陷入循环
②.为什么一定会是right先走?如果是left先走,最后相遇位置的 数值 会比key值要大,继续与key值位置交换,那么key值左边就有一个比key值大的数,这不符合要求
2 挖坑法
public static void quickSort(int[] array){quick(array,0,array.length-1);}private static void quick(int[] array, int start, int end) {//结束的标志 只有一个节点或者一个节点都没有if(start >= end) return ;//求基准int pivot = parttion(array,start,end);//递归左边quick(array,start,pivot-1);//递归右边quick(array,pivot+1,end);}private static int parttion(int[] array, int left, int right){int key = array[left];int i = left;while(left < right){while(left<right && array[right]>= key){//防止right越界right--;}//right下标一定是比key小的数据array[left] = array[right];while(left < right && array[left] <= key){left++;}//left 下标一定是比key大的数据array[right] = array[left];}//此时left与right相遇array[left] = key;return left;}
3. 前后指针法
public static void quickSort(int[] array){quick(array,0,array.length-1);}private static void quick(int[] array, int start, int end) {//结束的标志 只有一个节点或者一个节点都没有if(start >= end) return ;//求基准int pivot = parttion(array,start,end);//递归左边quick(array,start,pivot-1);//递归右边quick(array,pivot+1,end);}private static int parttion(int[] array, int left, int right){int d = left +1;int pivot = array[left];for(int i = left +1;i <= right;i++){if(array[i] < pivot){swap(array,i,d);d++;}}swap(array,d-1,left);return d-1;}
2.3.3 快速排序的优化
快速排序的标准实现与问题
标准的快速排序通常选择待排序序列的第一个(或最后一个)元素作为基准(pivot)。然后进行分区操作,将小于基准的元素移到其左边,大于基准的元素移到其右边。
这种选择方式在以下情况下会产生问题:
输入序列已经有序或近乎有序:例如,对 [1, 2, 3, 4, 5, 6, 7, 8] 进行排序。如果每次都选第一个元素(1, 2, 3...)作为基准,分区操作后,所有其他元素都会在基准的右边,左边子序列为空。
输入序列是逆序的:情况与上面类似。
导致的后果:
算法会退化为 O(n²) 的时间复杂度。因为递归树变得极度不平衡,深度接近 n,而每一层仍然需要 O(n) 的时间来进行分区操作。这完全丧失了快速排序“分而治之”的平均 O(n log n) 优势。
三数取中法(Median-of-Three)的优化
“三数取中法”正是为了解决上述问题而设计的一种优化策略。
核心思想:
不盲目地选择第一个或最后一个元素作为基准,而是从子数组的首、中、尾三个位置的元素中,选取其中大小居中的那个值作为基准。
具体步骤:
假设要对子数组 arr[left...right] 进行分区:
计算中间位置:mid = left + (right - left) / 2 (这样可以有效防止整数溢出)。
比较 arr[left], arr[mid], arr[right] 这三个数。
将这三个数的中位数(即大小排在中间的那个数)选为本次分区的基准。
如何比较并交换:
一个常见的实现模式是:
确保 arr[left] 是这三个数中最小的。
确保 arr[right] 是这三个数中最大的。
那么,arr[mid] 自然就是中位数。
2.3.4 快速排序的非递归实现
public static void quickSorNor(int[] array){Stack<Integer> stack = new Stack<>();int left = 0;int right = array.length-1;int piovt = parttion(array,left,right);if(piovt-1 > left){stack.push(left);stack.push(piovt-1);}if(piovt +1 < right){stack.push(piovt+1);stack.push(right);}while(!stack.isEmpty()){right = stack.pop();left = stack.pop();piovt = parttion(array,left,right);if(piovt-1 > left){stack.push(left);stack.push(piovt-1);}if(piovt +1< right){stack.push(piovt +1);stack.push(right);}}}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
2.4 归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
public static void mergeSort(int[] array){mergeSortFunc(array,0,array.length-1);}private static void mergeSortFunc(int[] array, int left, int right) {if(left >= right) return ;int mid = (left + right)/2;mergeSortFunc(array,left,mid);mergeSortFunc(array,mid+1,right);//合并两个merge(array,left,right,mid);}private static void merge(int[] array, int left, int right, int mid) {int s1 = left;int s2 = mid+1;int[] tmpArr = new int[right-left+1];int k = 0;//确保两个区间都有数据while(s1 <= mid && s2 <= right){if(array[s1] > array[s2]){tmpArr[k++] = array[s2++];}else{//这里的条件是 array[s1] <= array[s2] //所以相同的数字会保持原来的顺序tmpArr[k++] = array[s1++];}}//假如一个区间完成了,另一个区间还没有完成while(s1 <= mid){tmpArr[k++] = array[s1++];}while(s2 <= right){tmpArr[k++] = array[s2++];}//到这里tmpArr里数据就是有序了for (int i = 0; i < tmpArr.length ; i++) {array[i+left] = tmpArr[i];}}
归并排序特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。 2. 时间复杂度:O(N*logN) 3. 空间复杂度:O(N) 4. 稳定性:稳定
2.4.1 海量数据归并问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G 因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行 2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
3 其他排序
3.1 计数排序
最上面是一组无序数据,创建一个计数的数组count,在 i下标记录 i出现了几次
然后 打印 i 值 count [ i ]次,打印出来的就是排序好的数据
适合排序某个集中的区间的数据
public static void Sort(int[] array){int minVal = array[0];int maxVal = array[0];for (int i = 0; i < array.length; i++) {if(array[i] < minVal){minVal = array[i];}}for (int i = 0; i < array.length; i++) {if(array[i] > maxVal){maxVal = array[i];}}//创建计数数组的大小int [] count = new int[maxVal-minVal+1];//遍历原来数组,开始计数for (int i = 0; i < array.length; i++) {count[array[i]-minVal] ++;}//遍历count数组,把当前元素写回原来数组for (int i = 0; i < count.length; i++) {int index = 0; //重新表示array数组的下标while(count[i] > 0){array[index] = i + minVal;index ++;count[i] --;}}}
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 2. 时间复杂度:O(MAX(N,范围)) 3. 空间复杂度:O(范围)