算法优劣:
- 执行效率
- 内存消耗,看是否是原地排序(既不需要额外的空间)
- 稳定性 ,即相同元素,如果排序前 a1 在 a2 前,排序完依旧是这样
根据比较来进行排序的算法有:冒泡,插入,选择 。
一 基于比较和交换的算法:
1 冒泡算法:
每次通过比较来判断与后一位的大小关系,来确定是否交换,每次内层循环结束,表明有一个最大的或者是最小的被筛选出来。如果某次循环没有进行过任何的交换,说明此时的数组已经是有序的。
代码:
public void bubbleSore() {
int arr[] = {10,8,7,5, 4, 3, 2, 1};
//外层for循环控制的是冒泡次数
for (int i = 0; i < arr.length; i++) {
boolean flag = false;
for (int j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if(!flag){
System.out.println("没有数据交换");
break;
}
}
System.out.println(Arrays.toString(arr));
}
算法分析:
- 冒泡排序是原地算法,并不涉及到额外的空间
- 是稳定排序,不会改变相同数据的顺序
- 最好情况是原数组直接就是有序的,时间复杂度为O(n),如果原数组是倒序的话,需要O(n),平均为O(n)
2 插入排序:
首先, 我们将数组中的数据分为两个区间, 已排序区间和未排序区间。 初始已排序区间只有一个元素, 就是数组的第一个元素。 插入算法的核心思想是取未排序区间中的元素, 在已排序区间中找到合适的插入位置将其插入, 并保证已排序区间数据一直有序。 重复这个过程, 直到未排序区间中元素为空, 算法结束。
代码:
int arr[] = {10,8,11,5, 4, 3, 2, 1};
//循环插入每个元素
for (int i=1;i<arr.length;i++){
int value = arr[i];
int j = i-1;
//将每个元素与已经插入的从后向前进行比较,符合条件 ,则向后移 ,直到遇到不符合条件的
for (;j>=0;j--){
if(value<arr[j]){
arr[j+1] = arr[j];
}else{
break;
}
}
//找到正确的插入点,赋值
arr[j+1] = value;
}
System.out.println(Arrays.toString(arr));
算法分析:
- 插入排序是原地算法,并不涉及到额外的空间
- 是稳定排序,不会改变相同数据的顺序
- 最好情况是原数组直接就是有序的,时间复杂度为O(n),如果原数组是倒序的话,需要O(n*n),平均为O(n*n)
3 选择排序:
分已排序区间和未排序区间。 但是选择排序每次会从未排序区间中找到最小的元素, 将其放到已排序区间的末尾
代码:
//外层 i 控制的是已经排好序的序列
public void selectSort(){
int arr[] = {10, 8, 11, 5, 4, 3, 2, 1};
for (int i = 0; i<arr.length;i++){
int min = i ;
for (int j=i;j<arr.length;j++){
if(arr[j]<arr[min]){
min = j ;
}
}
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
System.out.println(Arrays.toString(arr));
}
算法分析:
- 选择排序是原地算法,并不涉及到额外的空间
- 不是稳定排序,由于会改变相同元素大小
- 最坏,最好 ,平均都是O(n * n )
上述算法总结:
上述算法都是适用于小规模的数据结构,其中算法选择 插入 > 冒泡 > 选择
选择排序在最后一位的原因是因为不是稳定排序,且没有优化的可能。
插入比冒泡好的原因是:在内层循环内,冒泡需要进行4个赋值操作,插入需要一个赋值操作。下表格为对应的时间耗时:
(环境:Mac prol 8G 内存,不同环境运行时间不同,但是结果应该是一致的)
10000 | 100000 | |
插入排序 | 33ms | 2070m |
冒泡排序 | 46ms | 3251m |
4 归并排序:
把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
这里我们使用递归的编程技巧。
首先找到递推公式:mergeSort(start,end) = merge(mergeSort(start,middle),(middle+1,end))
终止条件:start >= end
public void mergeSort() {
int arr[] = {11, 8, 3, 9, 7, 1, 2, 5};
mergeSort(arr, 0, arr.length - 1);
}
public void mergeSort(int[] arr, int start, int end) {
if (start >= end) {
return;
}
//找出中间点,将数组分为俩部分
int middle = (start + end) / 2;
//对前一部分数组进行排序
mergeSort(arr, start, middle);
//对后一部分进行排序
mergeSort(arr, middle + 1, end);
//进行数组的合并
merge(arr, start, middle, end);
}
public void merge(int[] arr, int start, int middle, int end) {
int length = end - start + 1;
//创建临时空间,用来存储
int[] tempArr = new int[length];
int curr = 0;
int firstCursor = start;
int secondCursor = middle + 1;
while (firstCursor <= middle && secondCursor <= end) {
if (arr[firstCursor] < arr[secondCursor]) {
tempArr[curr++] = arr[firstCursor++];
} else {
tempArr[curr++] = arr[secondCursor++];
}
}
//将剩下的放入临时数组
while (firstCursor <= middle) {
tempArr[curr++] = arr[firstCursor++];
}
while (secondCursor <= end) {
tempArr[curr++] = arr[secondCursor++];
}
//将临时数组的数据替换到原数组
for (int k = 0; k < length; k++) {
arr[start + k] = tempArr[k];
}
System.out.println(Arrays.toString(tempArr));
}
算法分析:
- 归并排序是非原地算法,涉及到额外的空间,为O(n)
- 是稳定排序,不会改变相同元素大小
- 最坏,最好 ,平均都是O(nlogn)
5 快速排序:
如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间的任意一个数据作为 pivot(分区点)。
我们遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的。
代码:
public void quickSort() {
int arr[] = {11, 8, 3, 9, 7, 1, 2, 5};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
private void quickSort(int[] arr, int start, int end) {
if (start >= end) {
return;
}
int partition = partition(arr, start, end);
quickSort(arr, start, partition - 1);
quickSort(arr, partition + 1, end);
}
// 我们将start 和 end中间的数组分为俩个部分,一个是已经处理过的,即小于pivot 的,另一个是未进行处理的
private int partition(int[] arr, int start, int end) {
//
int pivot = arr[end];
//cursor 即标记处理到哪了,cursor 前面的都是 < pivot 的
int cursor = start;
for ( int j = start;j<end;j++){
//如果小于pivot ,将它与cursor 进行交换
if(arr[j]<pivot){
int temp = arr[cursor];
arr[cursor] = arr[j];
arr[j] =temp;
cursor ++;
}
}
//将end 于cursor 进行交换
int temp = arr[cursor];
arr[cursor] = arr[end];
arr[end] =temp;
return cursor;
}
算法分析:
- 快速排序是原地算法,
- 是非稳定排序,会改变相同元素大小
- 最坏为O(n*n),,最好 ,平均都是O(nlogn) 取决于分区点的选择
区分点的选择:可以使用三数取中法(即从首,末,中各取一个数,从三个数中选择一个中间的数),五数取中发 , 还可以使用随机选择法
二 线性排序,复杂度为O(n)
1 桶排序:
首先,我们来看桶排序。桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序(可以使用快排)。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
要求:桶与桶直接天然有序,且数据在每个桶内均匀分布,如果不均匀分布,会导致数据多的桶排序的时间复杂度很高。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
2 计数排序:
特殊的桶排序,假设元素的范围大小为0-m,那么桶的个数就是m +1 。且每个桶里存储的是相同元素的个数。
我们假设现在有10个学生,每个学生都有一个成绩,成绩范围为0-9 , 我们怎么使用计数排序来实现呢。
public static void countingSort(){
int []arr = new int[10];
Random random = new Random();
for (int i = 0;i<10 ;i++){
arr[i] = random.nextInt(9);
}
System.out.println("sort before: " + Arrays.toString(arr));
int[] bucketArr = new int[10];
//将对应桶内数据的个数加1
for (int i = 0 ;i<10; i++){
bucketArr[arr[i]] ++ ;
}
//将每个bucket中的数据为前一个加上当前的
for (int i=1;i<10;i++){
bucketArr[i] = bucketArr[i] + bucketArr[i-1];
}
//这里是为了展示,也可以新建一个临时数组,从后往前遍历来存储排序之后的数组
for (int i = 9 ;i>=0 ;i--){
int value = arr[i];
int rank = bucketArr[value];
System.out.println("value:" + value + " " +"rank: " + rank);
bucketArr[value] = bucketArr[value] -1 ; ;
}
}
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
3 基数排序:
比较两个数,我们只需要比较高位,高位相同的再比较低位。而且每一位的数据范围不能太大。例子:对手机号进行排序,我们可以对十一位分别进行排序,先排序末尾的,在依次使用进行排序, 最后得到的就是从小到大的列表。
限制:对于每位的排序算法必须是稳定排序的,且每一位都可与分割出独立的位,且位与位中间与有递进关系,如果数据a的高位比b的数据高位高,那么剩下的地位就不用比较了。
上述三种算法的时间复杂度都是O(n) 。
三 总结:
实现一个比较好的排序算法(一般为logn):
1 我们可以根据不同的数据量使用不同的算法,如小数据量可以使用归并排序,大数据量可以使用快速排序。