目录
- 一、直接插入排序
- 二、希尔排序
- 三、选择排序
- 四、堆排序
- 五、冒泡排序
- 六、快速排序
- 6.1
- 挖坑法
- Hoare 法
- 前后遍历法
- 6.2基准值的选择
- 6.3三数取中法示例
- 6.4直接插入法排序优化示例
- 6.5非递归实现快速排序
- 七、归并排序
- 7.1归并排序代码示例
- 7.2非递归方法实现归并排序
- 八、海量数据的排序问题
- 关于各种排序算法的时间复杂度、空间复杂度及稳定性列表
- 其他非基于比较的排序
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
一、直接插入排序
整个区间被分为
- 有序区间
- 无序区间
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i - 1;
for ( ; j >= 0 ; j--) {
if (arr[j] > tmp){
arr[j + 1] = arr[j];
}else{
//arr[j + 1] = tmp;//只要j回退的时候,遇到了比tmp小的元素,就结束了这次的比较
break;
}
}
//j回退到了小于0的地方
arr[j + 1] = tmp;
}
}
public static void main(String[] args) {
int[] array = {6,10,9,3,5};
insertSort(array);
System.out.println(Arrays.toString(array));
}
}
输出结果:
时间复杂度:最好O(N)
–>数据本身是有序的;
* 最坏O(N^2)
;–>数据逆序;
* 当一组数据,数据量不大且趋近于有序,此时用插入排序时间更快,越有序越快。
空间复杂度:O(1)
;
稳定性:稳定的排序;
一个稳定的排序可以实现为不稳定的排序,但是一个本身就不稳定的排序不可以变成稳定的排序。
二、希尔排序
希尔排序法又称缩小增量法。
希尔排序法的本质是插入排序,只不过是将待排序的序列按某种规则分成几个子序列,分别对几个子序列进行直接插入排序。这个规则就是增量,增量选取很重要,增量一般选序列长度一半,然后逐半递减,直到最后一个增量为1,为1相当于直接插入排序。
public static void shell(int[] arr,int gap){
for (int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i - gap;
for (; j >= 0 ; j-=gap) {
if(tmp < arr[j]){
arr[j + gap] = arr[j];
}else{
break;
}
}
arr[j+gap] = tmp;
}
}
public static void shellSort(int[] arr) {
int gap = arr.length;
//增量在缩小,最后一组的增量为1
while (gap > 1){
gap /= 2;
shell(arr,gap);
}
shell(arr,1);
}
public static void main(String[] args) {
int[] array = {9,1,2,5,7,4,8,6,3,5};
shellSort(array);
System.out.println(Arrays.toString(array));
输出结果:
时间复杂度:O(n^1.3 - n^1.5);
空间复杂度:O(1)
;
稳定性:不稳定;
看在比较的过程中,是否发生了跳跃式的交换,如果发生了跳跃式的交换,那么就是不稳定的排序。
三、选择排序
从指定元素开始,遍历其后面的元素,如遇到后面的元素比指定元素小,则进行交换,直到把整个数组遍历完,直到全部指定待排序的数据元素排完 。
public static void swap(int[] array,int i ,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1;; j <array.length ; j++) {
if(array[j] < array[i]){
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
//优化
//遍历完指定元素后面的元素找到最小的一个数和其进行交换
public static void selectSort1(int[] array) {
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i + 1; j <array.length ; j++) {
if(array[j] < array[min]){
min = j;
}
}
//两元素进行交换的函数
swap(array,i,min);
}
}
public static void main(String[] args) {
int[] array = {9,1,2,5,7,4,8,6,3,5};
selectSort(array);
System.out.println(Arrays.toString(array));
稳定性:不稳定的排序
时间复杂度:O(N^2)
,时间复杂度不等于代码的运行时间
空间复杂度:O(1)
。
四、堆排序
基本原理也是选择排序,只是不再使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。
注意: 排升序要建大堆;排降序要建小堆
堆排序动画演示
public static void swap(int[] array,int i ,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void creatHeap(int[] arr){
for (int parenr = (arr.length -1-1)/2; parenr >= 0 ; parenr--) {
shiftDown(arr,parenr,arr.length);
}
}
public static void heapSort(int[] arr){
//建堆O(n)
creatHeap(arr);
int end = arr.length - 1;
//交换然后调整O(n*log n)
while (end > 0){
swap(arr,0,end);
shiftDown(arr,0,end);
end--;
}
}
public static void shiftDown(int[] arr,int parent,int len){
int child = 2 * parent + 1;
while (child < len){
if (child+1 < len && arr[child] < arr[child + 1]){
child++;
}
if (arr[child] > arr[parent]){
swap(arr,child,parent);
parent = child;
child = 2 * parent +1;
}else {
break;
}
}
}
public static void main(String[] args) {
int[] array = {9,1,2,5,7,4,8,6,3,5};
heapSort(array);
System.out.println(Arrays.toString(array));
稳定性:不稳定;
空间复杂度:O(1)
;
时间复杂度:O(n*log n)
。
五、冒泡排序
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。
public static void swap(int[] array,int i ,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
boolean flg = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j+1] < arr[j]){
swap(arr,j+1,j);
flg = true;
}
}
//如果走完一遍循环之后,flg依旧为false,则没有进行交换,此时就可以提前结束循环,提高了代码的运行时间。
if (flg == false){
break;
}
}
}
public static void main(String[] args) {
int[] array = {9,1,2,5,7,4,8,6,3,5};
bubbleSort(array);
System.out.println(Arrays.toString(array));
时间复杂度:O(N^2)
;有序情况下:O(N)
;
空间复杂度:O(1)
;
稳定性:稳定。
六、快速排序
原理:
- 从待排序区间选择一个数,作为基准值(
pivot
); -
Partition
: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边; - 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度
== 1
,代表已经有序,或者小区间的长度== 0
,代表没有数据。
6.1
挖坑法
其主要思想为:选取数组中的第一个元素tmp
作为比较的对象,然后先将整个数组从后(end位置)往前遍历:如果后面的值比tmp
值大,则end
往前移动,直到遇到比tmp
小的元素,则将此位置的值移动到第一个元素所在的位置,然后从该位置有从前(start)往后遍历,如果start的值小于tmp
,则start往后移动,直到遇到比tmp
大的元素,则将此位置的值移动到end所在的位置,按照此方法,依次遍历完整个数组,即可找到基准值所在位置。
public static void quickSort1(int[] arr) {
quick(arr, 0, arr.length - 1);
}
public static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
int pivot = partition(arr, left, right);
quick(arr, left, pivot - 1);
quick(arr, pivot + 1, right);
}
public static int partition(int[] arr, int start, int end) {
int tmp = arr[start];
while (start < end) {
//如果数组最后面的数比tmp大,则end往前走
while (start < end && arr[end] >= tmp) {
end--;
}
//end就遇到了小于tmp的值
arr[start] = arr[end];
while (start < end && arr[start] <= tmp) {
start++;
}
//start就遇到了大于tmp的值
arr[end] = arr[start];
}
//将tmp的值放到相遇start和end的位置
arr[start] = tmp;
return start;
}
时间复杂度:最好【每次可以均匀的分割待排序序列】:O(n*log n)
最坏:【数据有序或者逆序的情况】O(n^2)
;
空间复杂度:最好[就是树的高度]:O(log n)
最坏:[退化成一颗单分支的树]:O(n)
;
稳定性:不稳定;
Hoare 法
private static int partition(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while (i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
while (i < j && array[i] <= pivot) {
i++;
}
swap(array, i, j);
}
swap(array, i, left);
return i;
}
前后遍历法
private static int partition(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, left);
return d;
}
6.2基准值的选择
1.随机选取基准法:有可能每次随机的数据,作为基准的时候,也会出现单分支的情况;
2.选择边上(左或者右)挖坑法;
3.几数取中(例如三数取中):array[left], array[mid], array[right]
大小是中间的为基准值。
4.把基准值相同的数据,从两边移动跟前;
5.利用直接插入排序越有序越快的规则来进行优化。
6.3三数取中法示例
public static void quickSort1(int[] arr) {
quick(arr, 0, arr.length - 1);
}
public static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
//找基准之前找到中间大小的数 几数取中(使用三数取中法)
int midValue = findMidValue(arr, left, right);
swap(arr,midValue,left);//找到中间大小的数之后和最左边的数进行交换
int pivot = partition(arr, left, right);
quick(arr, left, pivot - 1);
quick(arr, pivot + 1, right);
}
//找基准之前找到中间大小的数 三数取中
public static int findMidValue(int[] arr, int start, int end) {
int mid = start + (end - start)/2;
// int mid = start + ((end - start)>>>1);
if (arr[start] < arr[end]){ //最前面的值小于最后面的值
if (arr[mid] < arr[start]){ //中间的值又小于最前面的值
return start; //则三数中中间大的值为start位置的值
}else if (arr[mid] > arr[end]){
return end;
}else {
return mid;
}
}else {//最前面的值大于最后面的值
if (arr[mid] > arr[start]){
return start;
}else if (arr[mid] < arr[end]){
return end;
}else {
return mid;
}
}
}
public static int partition(int[] arr, int start, int end) {
int tmp = arr[start];
while (start < end) {
while (start < end && arr[end] >= tmp) {
end--;
}
//end就遇到了小于tmp的值
arr[start] = arr[end];
while (start < end && arr[start] <= tmp) {
start++;
}
//start就遇到了大于tmp的值
arr[end] = arr[start];
}
//将tmp的值放到相遇start和end的位置
arr[start] = tmp;
return start;
}
6.4直接插入法排序优化示例
public static void insertSort2(int[] arr,int start,int end ) {
for (int i = 1; i < end; i++) {
int tmp = arr[i];
int j = i - 1;
for (; j >= start; j--) {
if (arr[j] > tmp) {
arr[j + 1] = arr[j];
} else {
//arr[j + 1] = tmp;//只要j回退的时候,遇到了比tmp小的元素,就结束了这次的比较
break;
}
}
//j回退到了小于0的地方
arr[j + 1] = tmp;
}
}
public static void quickSort1(int[] arr) {
quick(arr, 0, arr.length - 1);
}
public static void quick(int[] arr, int left, int right) {
if (left >= right) {
return;
}
//如果区间内的数字,在排序的过程当中,小于某个范围,可以使用直接插入排序
if (left - right + 1 <= 40) { //40这个数可以自己规定
//使用直接插入排序
insertSort2(arr,left,right);
return;
}
//找基准之前找到中间大小的数 几数取中(使用三数取中法)
int midValue = findMidValue(arr, left, right);
swap(arr,midValue,left);//找到中间大小的数之后和最左边的数进行交换
int pivot = partition(arr, left, right);
quick(arr, left, pivot - 1);
quick(arr, pivot + 1, right);
}
//找基准之前找到中间大小的数 三数取中
public static int findMidValue(int[] arr, int start, int end) {
int mid = start + (end - start)/2;
// int mid = start + ((end - start)>>>1);
if (arr[start] < arr[end]){ //最前面的值小于最后面的值
if (arr[mid] < arr[start]){ //中间的值又小于最前面的值
return start; //则三数中中间大的值为start位置的值
}else if (arr[mid] > arr[end]){
return end;
}else {
return mid;
}
}else {//最前面的值大于最后面的值
if (arr[mid] > arr[start]){
return start;
}else if (arr[mid] < arr[end]){
return end;
}else {
return mid;
}
}
}
public static int partition(int[] arr, int start, int end) {
int tmp = arr[start];
while (start < end) {
while (start < end && arr[end] >= tmp) {
end--;
}
//end就遇到了小于tmp的值
arr[start] = arr[end];
while (start < end && arr[start] <= tmp) {
start++;
}
//start就遇到了大于tmp的值
arr[end] = arr[start];
}
//将tmp的值放到相遇start和end的位置
arr[start] = tmp;
return start;
}
快速排序优化方法:
- 选择基准值很重要,通常使用几数取中法;
-
partition
过程中把和基准值相等的数也选择出来;
就是把数组中和基准值相等的数都移动到基准值的周围,这样与基准值相等的值都汇集到了一起,然后只需遍历与基准值不相等的前面和后面即可。 - 待排序区间小于一个阈值时(例如 40),使用直接插入排序;
6.5非递归实现快速排序
1.找基准
2.栈 划分之后把左右的数都放到队列中,划分前提:pivot左边有两个元素 pivot > left +1,右边有两个元素
3.判断栈是否为空,如果栈不为空,则依次弹出栈中的两个元素,分别作为数组的右边元素和左边元素,然后再找基准,直到栈为空为止。
public static void quickSort(int[] arr) {
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = arr.length - 1;
int pivot = partition(arr, left, right);
//划分之后把左右的数对都放到栈当中
//前提:pivot左边有两个元素 右边有两个元素
if (pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
while (!stack.isEmpty()) {
right = stack.pop();
left = stack.pop();
pivot = partition(arr, left, right);
if (pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
七、归并排序
归并排序(MERGE-SORT
)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer
)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
首先,先写一下两个有序数组合并为一个有序数组的代码:
public static int[] mergeArray(int[] arr1, int[] arr2) {
//判断数组是否为空
int[] tmp = new int[arr1.length + arr2.length];
int k = 0;//代表tmp数组的下标
int s1 = 0;
int e1 = arr1.length - 1;
int s2 = 0;
int e2 = arr2.length - 1;
while (s1 <= e1 && s2 <= e2) {
if (arr1[s1] <= arr2[s2]) {
tmp[k] = arr1[s1];
k++;
s1++;
} else {
tmp[k++] = arr2[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = arr1[s1++];
}
while (s2 <= e2) {
tmp[k++] = arr2[s2++];
}
return tmp;
}
public static void main(String[] args) {
int[] array1 = {1,3,5,7,9};
int[] array2 = {2,4,6,8,10};
int[] ret = mergeArray(array1,array2);
System.out.println(Arrays.toString(ret));
}
输出结果:
7.1归并排序代码示例
public static void mergeSort(int[] arr) {
mergeSortInternal(arr, 0, arr.length - 1);
}
private static void mergeSortInternal(int[] arr, int low, int high) {
if (low >= high) {
return;
}
int mid = low + ((high - low) >>> 1);//无符号右移一位
//左边
mergeSortInternal(arr, low, mid);
//右边
mergeSortInternal(arr, mid + 1, high);
//合并
merge(arr, low, mid, high);
}
private static void merge(int[] arr, int low, int mid, int high) {
int s1 = low;
int e1 = mid;
int s2 = mid + 1;
int e2 = high;
int[] tmp = new int[high - low + 1];
int k = 0;
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]) {
tmp[k++] = arr[s1++];
// k++;
// s1++;
} else {
tmp[k++] = arr[s2++];
}
}
while (s1 <= e1) {
tmp[k++] = arr[s1++];
}
while (s2 <= e2) {
tmp[k++] = arr[s2++];
}
//拷贝tmp数组的元素放到arr里面
for (int i = 0; i < k; i++) {
arr[i + low] = tmp[i];
}
}
public static void main(String[] args) {
int[] arr = {3,5,1,2,34,0,98,-1,56};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
输出结果:
时间复杂度:O(n*logn)
;
空间复杂度:O(n)
;
稳定性:稳定的排序。
7.2非递归方法实现归并排序
public static void mergeSort1(int[] arr) {
int nums = 1;//每组数据的个数
while (nums < arr.length){
//数组每次都要进行遍历,确定要归并的区间
for (int i = 0; i < arr.length; i+=nums * 2) {
int left = i;
int mid = left + nums - 1;
if (mid >= arr.length){//防止越界
mid = arr.length - 1;
}
int right = mid + nums;
if (right >= arr.length){//防止越界
right = arr.length - 1;
}
//下标确定之后,合并
merge(arr,left,mid,right);
}
nums *= 2;
}
}
public static void main(String[] args) {
int[] arr = {3,5,1,2,34,0,98,-1,56};
mergeSort1(arr);
System.out.println(Arrays.toString(arr));
}
输出结果:
八、海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序。
前提:内存只有 1G
,需要排序的数据有 100G
;
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序。
- 先把文件切分成 200 份,每个 512 M;
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以;
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了。
关于各种排序算法的时间复杂度、空间复杂度及稳定性列表
排序算法 | 最好时间复杂度 | 最坏时间复杂度 | 最好空间复杂度 | 最坏空间复杂度 | 稳定性 |
直接插入排序 | O(N) | O(N^2) | O(1) | O(1) | 稳定 |
希尔排序 | O(N^1.3) | O(N^1.5) | O(1) | O(1) | 不稳定 |
选择排序 | O(N^2) | O(N^2) | O(1) | O(1) | 不稳定 |
堆排序 | O(NlogN) | O(NlogN) | O(1) | O(1) | 不稳定 |
冒泡排序 | O(N^2) | O(N^2) | O(1) | O(1) | 稳定 |
快速排序 | O(NlogN) | O(N^2) | O(logN) | O(N) | 不稳定 |
归并排序 | O(NlogN) | O(NlogN) | O(N) | O(N) | 稳定 |
其他非基于比较的排序
基数排序
桶排序
计数排序
public static void countSort(int[] arr){
int maxValue = arr[0];
int minValue = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] < minValue){
minValue = arr[i];
}
if (arr[i] > maxValue){
maxValue = arr[i];
}
}
int[] count = new int[maxValue-minValue + 1];
for (int i = 0; i < arr.length ; i++) {
int index = arr[i];
//为了空间的合理使用,需要减去minValue
count[index - minValue]++;
}
//说明在计数数组当中已经把arr数组中每个数据出现的次数已经统计好了
//接下来遍历计数数组把数据写回arr即可
int indexArray = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0){
arr[indexArray] = i + minValue;
count[i]--;//拷贝一个之后,此时也就是少一个
indexArray++;//
}
}
}
计数排序 一般适用于有n个数;
时间复杂度:O(N)
;
空间复杂度:O(M)
M代表当前数据的范围;
稳定性:不稳定, 当前代码是不稳定的,但是本质是稳定的;