排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。用一张图概括:、
直接插入排序的特点:
在查找插入位置时改为折半查找,即为折半插入排序。
void binary_insertion_sort_int(int *arr,int n) { for(int i = 1;i < n;i++) { int key = arr[i]; int left = 0; int right = i - 1; while(left <= right) { int mid = (left + right) / 2; if(key < arr[mid]) right = mid - 1; else left = mid + 1; } for(int j = i - 1; j >= left; j--) arr[j + 1] = arr[j]; arr[left] = key; }}
首先取长度的一半作为增量的步长d1,把表中全部记录分成d1个组,把步长隔d1的记录放在同一组中再进行直接插入排序;然后再取d1的一半作为步长,重复插入排序操作。
void shell_sort_int(int *arr,int n) { for(int dk = n / 2; dk >= 1; dk >>= 1) { for(int i = dk; i < n;i++) { int key = arr[i]; int j = i - dk; for(; j >=0 && arr[j] > key; j -= dk) { arr[j + dk] = arr[j]; } arr[j + dk] = key; } }}
二路归并排序是分治法的应用,模式如下:
关于两个有序列表之间的合并,只需要复制到辅助数组中,根据大小关系两两比较再放入原始数组中,这种合并方法有很多变式。二路归并排序的特点如下:
void merge(int *arr,int *help,int left,int mid,int right) { for(int i = left; i <= right;i++) help[i] = arr[i]; //把A中元素复制到B中,借助B处理 int i = left; int j = mid + 1; int k = left; while(i <= mid && j <= right) { //记录有多少个共同的 if(help[i] < help[j]) arr[k++] = help[i++]; //较小值复制到A中 else arr[k++] = help[j++]; } while(i <= mid) arr[k++] = help[i++]; //某表未检测完直接复制 while(j <= right) arr[k++] = help[j++];}void merge_sort_int(int *arr,int *help,int left,int right) { if(left < right) { int mid = ((right - left) >> 1) + left; merge_sort_int(arr,help,left,mid); merge_sort_int(arr,help,mid + 1,right); merge(arr,help,left,mid,right); }}
原地归并排序不需要辅助数组就可以归并,关键在于merge函数。思路是:
上面操作说明第二个序列(start,end-1)的元素都应该在i之前-采用循环移位的办法移到i前面。
例如0 1 5 6 9 | 2 3 4 7 8:
/*三个辅助函数*/void swap(int *a,int *b) { int tmp = *a; *a = *b; *b = tmp;}/*长度为n的数组翻转*/void reverse(int *arr,int n) { int i = 0; int j = n - 1; while(i < j) { swap(&arr[i],&arr[j]); i++; j--; }}/** * 将含有n个元素的数组左循环移位i位 */void rotation_left(int *arr,int n,int i) { reverse(arr,i); reverse(arr + i,n - i); reverse(arr,n);}void merge_inplace(int *arr,int left,int mid,int right) { int i = left,j = mid + 1, k = right; while(i < j && j <= k) { while(i < j && arr[i] <= arr[j]) i++; int step = 0; while(j <= k && arr[j] <= arr[i]) { j++; step++; } //arr+i为子数组,j-i表示子数组元素个数,j-i-step表示左循环移位的位数(把前面的元素移到后面去) rotation_left(arr + i,j- i,j - i - step); }}void merge_sort_inplace(int *arr,int left,int right) { if(left < right) { int mid = (left + right) / 2; merge_sort_inplace(arr,left,mid); merge_sort_inplace(arr,mid+1,right); merge_inplace(arr,left,mid,right); }}
冒泡排序总共要进行n-1趟遍历,每次都能确定一个元素最终的位置(剩余序列中的最值)。在一个序列中,正向遍历能确定一个最大值,反向遍历确定一个最小值,每次序列减少一个元素。它的特点如下:
void bubble_sort_int(int *arr,int n) { for(int i = 0; i < n - 1; i++) { //n-1趟冒泡,每次确定一个元素,每次确定小值元素 bool flag = false; for(int j = n- 1; j > i; j--) { if(arr[j - 1] > arr[j]) { //有逆序 swap(&arr[j - 1],&arr[j]); flag = true; //发生交换 } } if(flag == false) return ; //表明本次遍历没有交换,则表已经有序 }}
快速排序的核心在于划分操作,它的性能也取决于划分操作的好坏,快排是所有内部排序算法中平均性能最好的。它的特点如下:
快速排序的主框架如下:
void quick_sort_int(int *arr,int left,int right) { if(left < right) { int pos = partition1(arr,left,right); //划分 quick_sort_int(arr,left,pos - 1); quick_sort_int(arr,pos + 1,right); }}
两指针分别从首尾向中间扫描。这里主要考虑的是选取pivot的策略,可能是首元素或尾元素,甚至是三数取中或者随机取某个数。思路是将数据以pivot作划分,右侧小于pivot移到左侧,左侧大于pivot移到右侧
此方法可以用来确定第k大或第k小的元素。
int partition(int *arr,int left,int right) { int pivot = arr[left]; while(left < right) { while(left < right && pivot <= arr[right]) right--; arr[left] = arr[right]; //比pivot值小的移到左端 while(left < right && pivot >= arr[left]) left++; arr[right] = arr[left]; //比pivot值大的移到右端 } arr[left] = pivot; //left == right return left;}
其实还有一种交换策略: 把左右端不满足条件的元素交换,最后循环终止的元素应该与开始元素进行交换。这种方法结合前后两个策略: 既双向遍历又使用了交换操作。
int partition1(int *arr,int left,int right) { int start = left; int pivot = arr[left]; while(left < right) { while(left < right && pivot <= arr[right]) right--; while(left < right && pivot >= arr[left]) left++; swap(&arr[left],&arr[right]); } swap(&arr[start],&arr[left]); return left;}
采用单向遍历: 两指针索引一前一后逐步向后扫描。它的策略是: 记录一个last指针,用于保存小于或等于pivot的元素。从左往右扫描,每遇见一个元素小于等于pivot即将它存储于last指针里,所以一趟划分过后last之前的元素(包括last,这取决于last的初始化)小于等于pivot。
可以看出该划分算法的特点: 从前向后遍历,last记录的是小于等于pivot的元素,这些元素相对顺序不变。若要大于等于pivot的元素相对顺序不变,可从后向前遍历,pivot取首元素。
int partition2(int *arr,int left,int right) { int pivot = arr[right]; int last = left -1; for(int i = left; i < right; i++) { if(arr[i] <= pivot) { ++last; swap(&arr[last],&arr[i]); } } swap(&arr[last + 1],&arr[right]); return last + 1;}
如果pivot不方便取,例如Dijkstra提出的荷兰三色旗问题要保证0,1,2三者有序,使用单纯的单向遍历会存在0和1顺序乱的情况,并且元素的重复次数比较多,所以Dijkstra提出了一种简单快速的三向划分法。
Dijkstra三向快速切分: 用于处理有大量重复元素的情形,减少递归时重复元素的比较的次数。即遍历数组一次,维护三个指针lt、cur、gt
这和荷兰三色旗是类似的问题,思路比较简单。要学会掌握用一个工作指针结合两个边界指针进行一次线性遍历,同样有很多变式。完整的三向划分快速排序代码如下:
void quick_sort_threeway(int *arr,int left,int right) { if(left < right) { int lt = left; int cur = left; int gt = right; int pivot = arr[left]; while(cur <= gt) { if(arr[cur] == pivot) { cur++; } else if(arr[cur] < pivot) { swap(&arr[cur],&arr[lt]); lt++; cur++; } else { swap(&arr[cur],&arr[gt]); gt--; } } //cur > gt则退出,直接形成了left..lt-1 lt..gt gt+1..right三个区间 quick_sort_threeway(arr,left,lt - 1); quick_sort_threeway(arr,gt + 1,right); } }
不过这种方法唯一的缺点就是在数组中重复元素不多的情况下比标准的二分法多使用了很多次交换。90年代,John Bently等人用一个聪明的方法解决了此问题,使得三向切分的快排比一般的排序方法都要快。
Bently用重复元素放置于子数组两端的方式实现了一个信息量最优的算法,这里额外的交换只用于和切分的pivot元素相等的元素,上面的额外交换是用于切分不相等的元素。
思路如下:
如图:
它的代码如下:
void quick_sort_threeway_fast(int *arr,int left,int right) { if(left < right) { int p = left , q = right + 1; int pivot = arr[left]; int i = left , j = right + 1; while(true) { while(arr[++i] < pivot) if(i == right) break; while(arr[--j] > pivot) if(j == left) break; if(i == j && arr[i] == pivot) swap(&arr[++p],&arr[i]); if(i >= j) break; swap(&arr[i],&arr[j]); if(arr[i] == pivot) swap(&arr[++p],&arr[i]); if(arr[j] == pivot) swap(&arr[--q],&arr[j]); } i = j + 1; for(int k = left; k <= p; k++) swap(&arr[k],&arr[j--]); for(int k = right; k >= q;k--) swap(&arr[k],&arr[i++]); quick_sort_threeway_fast(arr,left,j); quick_sort_threeway_fast(arr,i,right); }}
在元素个数比较小的时候使用直接插入排序,元素个数较多的适合主要在于pivot元素的选取策略。代码如下:
/** * 三数取中策略: 返回数组的下标 */int median3(int *arr,int i,int j,int k) { if(arr[i] < arr[j]) { if(arr[j] < arr[k]) return j; else { return (arr[i] < arr[k])? k : i; } } else { if(arr[k] < arr[j]) return j; else { return (arr[k] < arr[i])? k : i; } }}void optimal_sort_int(int *arr,int left,int right) { int n = right - left + 1; if(n <= THRESHOLD) { insertion_sort_int(arr,n); } else if(n <= 500) { //用三数取中排序 int pos = median3(arr,left, left + n / 2,right); swap(&arr[pos],&arr[left]); } else {//采取Tukey ninther 作为pivot元素 int eighth = n / 8; int mid = left + n / 2; int m1 = median3(arr,left,left + eighth,left + eighth * 2); int m2 = median3(arr,mid - eighth,mid,mid + eighth); int m3 = median3(arr,right - eighth * 2,right - eighth,right); int ninther = median3(arr,m1,m2,m3); swap(&arr[ninther],&arr[left]); } quick_sort_threeway_fast(arr,left,right);}
选择排序的基本思路: 每一趟要在后面的元素中确定一个最值元素,重复n - 1趟。它的特点如下:
void selection_sort_int(int *arr,int n) { int min; for(int i = 0; i < n - 1;i++) {//n-1趟选择排序,每次选择一个最小值 min = i; for(int j = i + 1; j < n;j++) { if(arr[min] < arr[j]) { min = j; //更新最小元素位置 } } if(min != i) swap(&arr[i],&arr[min]); //更新到的最小值与i位置交换 }}
堆排序是一种树形选择排序,利用完全二叉树中父节点和子节点的关系选择最值的元素。排序的步骤如下:
堆排序的特点是:
堆排序的主程序如下(注意0号不存储元素,实际存储从1开始,数组长度为len + 1):
void heap_sort_int(int *arr,int len) { build_max_heap(arr,len); for(int i = len; i > 1;i--) { swap(&arr[i],&arr[1]); //和堆顶元素交换,并输出堆顶元素 sink_down(arr,1,i - 1); //注意还剩余i-1个元素调整堆 }}
A 下标为k的堆的自顶向下操作
这种操作是为了保持根为下标k的元素的子树满足最大堆性质。如果某个节点比它们的两个子节点或之一更小,则以该节点为根的子树不满足最大堆性质。
调整思路: 把更小的元素由上至下下沉,以类似的方式维持其子节点的堆状态性质直到某子节点元素满足最大堆的状态。向下调整的时间与树高有关为O(h)
此操作用于构造堆,堆排序和删除操作(delMax),代码如下:
void sink_down(int *arr,int k,int len) { while(2 * k <= len) { int j = 2 * k; if(j <len && arr[j] < arr[j + 1]) j++; //取更大的元素 if(arr[k] > arr[j]) break; //根元素大于子节点,则满足最大堆性质无需调整 swap(&arr[k],&arr[j]); k = j; }}
B 下标为k的堆自底向上操作
这种操作是因为该节点的大小比其父节点更大,则需要进行向上调整,注意终止条件是k=1且父节点更大。
该操作用于在堆底插入新元素
void swim_up(int *arr,int k) { while(k > 1 && arr[k] > arr[k / 2]) { swap(&arr[k],&arr[k / 2]); k = k / 2; }}
总结起来堆和优先级队列的插入、删除和取最值还是比较简单的,关键还是在于这两个堆调整操作,代码量不大,但需要理解其运行逻辑。
以无序数组自底向上构造出一个最大堆,时间复杂度为O(n),实际存储从下标1开始,数组长度为len + 1。从n=len/2开始进行自顶向下调整操作
void build_max_heap(int *arr,int len) { for(int i = len / 2; i >= 1;i--) sink_down(arr,i,len);}
大根堆一般可用于求海量数据中最小的k个数: 即先读取k个元素构造最大堆,再依次读入数据。若当前数据比堆顶小,则替换堆顶;若当前数据比较大,不可能是最小的k个数 。对应地求最大的k个数一般用小根堆。
如下分别是用快排的划分算法和堆的性质取得最小的k个数
void print(int *arr,int left,int right) { for(int i = left; i<= right;i++) printf("%d ",arr[i]); printf("\n");}/*基于partition算法取最小的k个数*/void getleastk(int *arr,int n,int k) { int left = 0; int right = n - 1; int pos = partition(arr,left,right); while(pos != k - 1) { if(pos > k - 1) { right = pos - 1; pos = partition(arr,left,right); } else { left = pos + 1; pos = partition(arr,left,right); } } print(arr,0,k-1);}/*基于最大堆求最小的k个数*/void getmink(int *arr,int n,int k) { int b[k+1]; for(int i = 1; i <= k;i++) { b[i] = arr[i - 1]; } build_max_heap(b,k); for(int i = k; i < n;i++) { if(arr[i] > b[1]) continue; else { b[1] = arr[i]; sink_down(b,1,k); } } heap_sort_int(b,k); print(b,1,k);}
计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。
由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。
通俗地理解,例如有 10 个年龄不同的人,统计出有 8 个人的年龄比 A 小,那 A 的年龄就排在第 9 位,用这个方法可以得到其他每个人的位置,也就排好了序。当然,年龄有重复时需要特殊处理(保证稳定性),这就是为什么最后要反向填充目标数组,以及将每个数字的统计减去 1 的原因。
算法的步骤如下:
public class CountingSort implements IArraySort { @Override public int[] sort(int[] sourceArray) throws Exception { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int maxValue = getMaxValue(arr); return countingSort(arr, maxValue); } private int[] countingSort(int[] arr, int maxValue) { int bucketLen = maxValue + 1; int[] bucket = new int[bucketLen]; for (int value : arr) { bucket[value]++; } int sortedIndex = 0; for (int j = 0; j < bucketLen; j++) { while (bucket[j] > 0) { arr[sortedIndex++] = j; bucket[j]--; } } return arr; } private int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; }}
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序有两种方法:
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:
public class RadixSort implements IArraySort { @Override public int[] sort(int[] sourceArray) throws Exception { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); int maxDigit = getMaxDigit(arr); return radixSort(arr, maxDigit); } /** * 获取最高位数 */ private int getMaxDigit(int[] arr) { int maxValue = getMaxValue(arr); return getNumLenght(maxValue); } private int getMaxValue(int[] arr) { int maxValue = arr[0]; for (int value : arr) { if (maxValue < value) { maxValue = value; } } return maxValue; } protected int getNumLenght(long num) { if (num == 0) { return 1; } int lenght = 0; for (long temp = num; temp != 0; temp /= 10) { lenght++; } return lenght; } private int[] radixSort(int[] arr, int maxDigit) { int mod = 10; int dev = 1; for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) { // 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10) int[][] counter = new int[mod * 2][0]; for (int j = 0; j < arr.length; j++) { int bucket = ((arr[j] % mod) / dev) + mod; counter[bucket] = arrayAppend(counter[bucket], arr[j]); } int pos = 0; for (int[] bucket : counter) { for (int value : bucket) { arr[pos++] = value; } } } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrayAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; }}
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
public class BucketSort implements IArraySort { private static final InsertSort insertSort = new InsertSort(); @Override public int[] sort(int[] sourceArray) throws Exception { // 对 arr 进行拷贝,不改变参数内容 int[] arr = Arrays.copyOf(sourceArray, sourceArray.length); return bucketSort(arr, 5); } private int[] bucketSort(int[] arr, int bucketSize) throws Exception { if (arr.length == 0) { return arr; } int minValue = arr[0]; int maxValue = arr[0]; for (int value : arr) { if (value < minValue) { minValue = value; } else if (value > maxValue) { maxValue = value; } } int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1; int[][] buckets = new int[bucketCount][0]; // 利用映射函数将数据分配到各个桶中 for (int i = 0; i < arr.length; i++) { int index = (int) Math.floor((arr[i] - minValue) / bucketSize); buckets[index] = arrAppend(buckets[index], arr[i]); } int arrIndex = 0; for (int[] bucket : buckets) { if (bucket.length <= 0) { continue; } // 对每个桶进行排序,这里使用了插入排序 bucket = insertSort.sort(bucket); for (int value : bucket) { arr[arrIndex++] = value; } } return arr; } /** * 自动扩容,并保存数据 * * @param arr * @param value */ private int[] arrAppend(int[] arr, int value) { arr = Arrays.copyOf(arr, arr.length + 1); arr[arr.length - 1] = value; return arr; }}
文章来自
https://www.cnblogs.com/AsanoLy/p/16159171.html