Sorting algorithms
1.概述
排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
- 不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
- 内排序:所有排序操作都在内存中完成;
- 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度: 一个算法执行所耗费的时间。
- 空间复杂度:运行完一个程序所需内存的大小。
2.1快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
算法描述
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
- 从数列中挑出一个元素,称为
基准(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
public class test {
public static void main(String[] args) {
int[] a = {46, 30, 15, 90, 56, 17, 56, 15,4,5,8,74,5,4,5,1,5,44};
int start = 0;
int end = a.length - 1;
sort(a, start, end);
for (int anA : a) {
System.out.print(" "+anA);
}
}
public static void sort(int arr[], int low, int high) {
//因为涉及递归调用,因此在设计方法时传入排序的上下界
int l = low;//左指针
int h = high;//右指针
int baseNum = arr[low];
//选取最左侧的作为基准,并且进行比较的时候一定从基准相反方向开始比较
while (l < h) {//因为l和h的--是分开的,因此跳出时是l=h
//从右向左查找小于指定基数的数,找到之后跳出循环执行下面if循环,交换数据
while (l < h && arr[h] >= baseNum) {
h--;
}
//交换数据
if (l < h) {
int temp = arr[h];
arr[h] = arr[l];
arr[l] = temp;
l++;
}
//从左向右查找大于指定基数的数,找到后跳出循环执行下面if循环,交换数据
while (l < h && arr[l] <= baseNum)
l++;
//交换数据
if (l < h) {
int temp = arr[h];
arr[h] = arr[l];
arr[l] = temp;
h--;
}
}
//交换实际是,基准值在不停的通过与自身当前位置交换,将比他大的放到右侧,小的放到左侧
if (l > low) {//左右两部分再分别递归直到只剩一个,那么就是有序的了
sort(arr, low, l - 1);//low和high在不同的调用的时候会有相应的变化
}
if (h < high) {
sort(arr, l + 1, high);
}
}
}
2.2堆排序(Heap Sort)
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
- 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
- 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
- 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
package Test;
import java.util.Arrays;
public class Test {
public void heapSort(int[] arrays) {
for (int i = 0; i < arrays.length; i++) {
//建大根堆,数组初始对应的堆,0为根节点,1为左侧子节点,2为右侧,以此类推
createMaxdHeap(arrays, arrays.length - 1 - i);
//减i为减去后面已经有序的部分
//将当前无序区的堆顶记录R[0]和该区间的最后一个记录R[last]交换。
swap(arrays, 0, arrays.length - 1 - i);//0为堆顶的位置,即最大值
//打印每一次创建堆的过程
// System.out.println(Arrays.toString(arrays));
}
}
public void createMaxdHeap(int[] arrays, int lastIndex) {
for (int i = (lastIndex - 1) / 2; i >= 0; i--) {
//此处i计算的是倒数第二层,最右侧有子节点的节点的下标
// 保存当前正在判断的节点
int k = i;
// 若当前节点的左侧子节点存在
if (2 * k + 1 <= lastIndex) {//等于时为左节点为最后一个节点
// biggerIndex总是记录较大节点的值,先赋值为当前判断节点的左子节点
int biggerIndex = 2 * k + 1;
if (biggerIndex < lastIndex) {
// 若右子节点存在,否则此时biggerIndex应该等于 lastIndex
if (arrays[biggerIndex] < arrays[biggerIndex + 1]) {
// 若右子节点值比左子节点值大,则biggerIndex记录的是右子节点的值
biggerIndex++;
}
}
if (arrays[k] < arrays[biggerIndex]) {
// 若当前节点值比子节点最大值小,则交换2者得值,交换后将biggerIndex值赋值给k
swap(arrays, k, biggerIndex);
// k = biggerIndex;
}//else {
//break;
//}
}
}
}
public void swap(int[] arrays, int i, int j) {
int temp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = temp;
}
public static void main(String[] args) {
Test s = new Test();
int[] arrays = new int[] {5, 3, 6, 2, 1, 9, 4, 8, 7,10,12,11};
System.out.println("未排序的数组:" + Arrays.toString(arrays));
s.heapSort(arrays);
System.out.println("排序后的数组:" + Arrays.toString(arrays));
}
}
2.3冒泡排序(Bubble Sort)
原理:比较两个相邻的元素,将值大的元素交换至右端。
思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复第一趟步骤,直至全部排序完成。
第一趟比较完成后,最后一个数一定是数组中最大的一个数,所以第二趟比较的时候最后一个数不参与比较;
第二趟比较完成后,倒数第二个数也一定是数组中第二大的数,所以第三趟比较的时候最后两个数不参与比较;
依次类推,每一趟比较次数-1;
举例说明:要排序数组:int[] arr={6,3,8,2,9,1};
第一趟排序:
第一次排序:6和3比较,6大于3,交换位置: 3 6 8 2 9 1
第二次排序:6和8比较,6小于8,不交换位置:3 6 8 2 9 1
第三次排序:8和2比较,8大于2,交换位置: 3 6 2 8 9 1
第四次排序:8和9比较,8小于9,不交换位置:3 6 2 8 9 1
第五次排序:9和1比较:9大于1,交换位置: 3 6 2 8 1 9
第一趟总共进行了5次比较, 排序结果: 3 6 2 8 1 9
第二趟排序:
第一次排序:3和6比较,3小于6,不交换位置:3 6 2 8 1 9
第二次排序:6和2比较,6大于2,交换位置: 3 2 6 8 1 9
第三次排序:6和8比较,6大于8,不交换位置:3 2 6 8 1 9
第四次排序:8和1比较,8大于1,交换位置: 3 2 6 1 8 9
第二趟总共进行了4次比较, 排序结果: 3 2 6 1 8 9
第三趟排序:
第一次排序:3和2比较,3大于2,交换位置: 2 3 6 1 8 9
第二次排序:3和6比较,3小于6,不交换位置:2 3 6 1 8 9
第三次排序:6和1比较,6大于1,交换位置: 2 3 1 6 8 9
第二趟总共进行了3次比较, 排序结果: 2 3 1 6 8 9
第四趟排序:
第一次排序:2和3比较,2小于3,不交换位置:2 3 1 6 8 9
第二次排序:3和1比较,3大于1,交换位置: 2 1 3 6 8 9
第二趟总共进行了2次比较, 排序结果: 2 1 3 6 8 9
第五趟排序:
第一次排序:2和1比较,2大于1,交换位置: 1 2 3 6 8 9
第二趟总共进行了1次比较, 排序结果: 1 2 3 6 8 9
最终结果:1 2 3 6 8 9
N个数字要排序完成,总共进行N-1趟排序,每i趟的排序次数为(N-i)次
public class test {
public static void main(String[] args) {
int[] arr = {6, 3, 8, 2, 9, 1};
System.out.println("排序前数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
for (int i = 0; i < arr.length - 1; i++) {//外层循环控制排序趟数
for (int j = 0; j < arr.length - 1 - i; j++) {
//内层循环控制每一趟排序多少次,最后一个不用比
if (arr[j] > arr[j + 1]) {//将当前值与下一个比较,大于则交换
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
System.out.println();
System.out.println("排序后的数组为:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
2.4选择排序(SelectionSort)
原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕。
举例:数组 int[] arr={5,2,8,4,9,1};
第一趟排序: 原始数据:5 2 8 4 9 1
最小数据1,把1放在首位,也就是1和5互换位置,
排序结果:1 2 8 4 9 5
第二趟排序:
第1以外的数据{2 8 4 9 5}进行比较,2最小,
排序结果:1 2 8 4 9 5
第三趟排序:
除1、2以外的数据{8 4 9 5}进行比较,4最小,8和4交换
排序结果:1 2 4 8 9 5
第四趟排序:
除第1、2、4以外的其他数据{8 9 5}进行比较,5最小,8和5交换
排序结果:1 2 4 5 9 8
第五趟排序:
除第1、2、4、5以外的其他数据{9 8}进行比较,8最小,8和9交换
排序结果:1 2 4 5 8 9
public class test {
public static void main(String[] args) {
int[] arr = {1, 3, 2, 45, 65, 33, 12};
System.out.println("交换之前:");
for (int num : arr) {
System.out.print(num + " ");
}
//选择排序的优化
for (int i = 0; i < arr.length - 1; i++) {// 做第i趟排序
int k = i;
for (int j = k + 1; j < arr.length; j++) {// 选最小的记录
if (arr[j] < arr[k]) {
k = j; //记下目前找到的最小值所在的位置
}
}
//在内层循环结束,也就是找到本轮循环的最小的数以后,再进行交换
if (i != k) { //交换a[i]和a[k]
int temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
System.out.println();
System.out.println("交换后:");
for (int num : arr) {
System.out.print(num + " ");
}
}
}
2.5插入排序(Insertion Sort)
插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
import java.util.Arrays;
public class test {
public int[] insertSort(int[] arr){
for(int i=1; i<arr.length; i++){
for(int j=i; j>0; j--){//和前面有序的比较
if(arr[j]<arr[j-1]){//小于交换位置,前移
int temp = arr[j-1];
arr[j-1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
public static void main(String[] args) {
test s = new test();
int[] arr = { 11, 3, 29, 49, 30, 7, 50, 63, 46};
System.out.println("未排序的数组:" + Arrays.toString(arr));
s.insertSort(arr);
System.out.println("排序后的数组:" + Arrays.toString(arr));
}
}
2.6希尔排序(Shell Sort)
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
import java.util.Arrays;
public class test {
public static int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i - gap;//将同一组内排序
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
public static void main(String[] args) {
int[] arrays = new int[] { 11, 3, 29, 49, 30, 7, 50, 63, 46 };
System.out.println("未排序的数组:" + Arrays.toString(arrays));
ShellSort(arrays);
System.out.println("排序后的数组:" + Arrays.toString(arrays));
}
}
2.7归并排序(MERGE-SORT)
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
import java.util.Arrays;
public class test {
public static void main(String[] args) {
int[] arrays = new int[]{11, 3, 29, 49, 30, 7, 50, 63, 46};
System.out.println("未排序的数组:" + Arrays.toString(arrays));
mergesort(arrays);
System.out.println("排序后的数组:" + Arrays.toString(arrays));
}
public static void mergesort(int[] arr) {
// 在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
int[] temp = new int[arr.length];
sort(arr, 0, arr.length - 1, temp);
}
private static void sort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
sort(arr, left, mid, temp);// 左边归并排序,使得左子序列有序
sort(arr, mid + 1, right, temp);// 右边归并排序,使得右子序列有序
merge(arr, left, mid, right, temp);// 将两个有序子数组合并操作
}
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left;// 左序列指针
int j = mid + 1;// 右序列指针
int t = 0;// 临时数组指针
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
while (i <= mid) {// 将左边剩余元素填充进temp中
temp[t++] = arr[i++];
}
while (j <= right) {// 将右序列剩余元素填充进temp中
temp[t++] = arr[j++];
}
t = 0;
// 将temp中的元素全部拷贝到原数组中
while (left <= right) {
arr[left++] = temp[t++];
}
}
}
2.8计数排序(Counting Sort)
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
8.1 算法描述
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
2.9桶排序(Bucket Sort)
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
9.1 算法描述
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
2.10基数排序(Radix Sort)
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
10.1 算法描述
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始取每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点);
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。