一、冒泡排序
冒泡排序就是将相邻的两个数字比较,如果顺序错误就将两个数交换,每轮遍历后右侧位置为最大值。
public void bubbleSort(int[] arr){
for (int i = 0; i <arr.length ; i++) {
//每轮遍历后,第arr.length-i-1之后的元素都有序了,不用比较
//减少程序运行次数
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1])
swap(arr,j+1,j);
}
}
}
进行了两次循环,第一次循环进行n次,第二次循环近似为n次,所以时间复杂度为O(n^2);
由于可以当两个数相等的时候不进行交换,所以可以保证相等的数的相对位置不变,所以该排序算法是稳定的。
1.2冒泡排序的改进
优化一:由于最外层的最后一次循环时整个数组已经有序,所以最外层循环可以减少一次
优化二:我们可以设置一个标记符,当一个数组本身就是有序状态,或者在外层循环几次后已经有序了,那就不用继续比较,直接返回即可
public void bubbleSortImprove(int[] arr){
boolean flag=true;
for (int i = 0; i <arr.length-1 ; i++) {
flag=true;
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]){
swap(arr,j+1,j);
//若交换了,表明还是无序状态
flag=false;
}
}
//若flag没有改变,说明没有发生交换
if(flag)
return;
}
}
二、选择排序
选择排序就是循环遍历数组,每次挑选一个最小的数字放在最左端
public void selectSort(int[] arr){
int min=0;
for (int i = 0; i < arr.length-1; i++) {
min=i;
for (int j = i+1; j < arr.length; j++) {
if(arr[j]<arr[min])
min=j;
}
swap(arr,i,min);
}
}
这种排序算法的时间复杂度也是O(n^2);
选择排序也是一种不稳定的排序算法,比如对A80,B80,C70进行排序,第一步之后会变成C70,B80,A80,再次进行排序时也会是这个顺序,如果要保持稳定,那么就得增加移动次数,使得性能又进一步降低
2.2选择排序改进
我们每次都挑选一个最小的数在最左边,也可以同时挑选一个最大的数在最右边,两边向中间靠拢,就减少了执行次数。
public void selectSortImprove(int[] arr){
int min=0;
int max=0;
int left=0;
int right=arr.length-1;
while (left<right){
min=left;
max=left;
for (int i = left+1; i <=right ; i++) {
if(arr[i]<arr[min])
min=i;
if(arr[i]>arr[max])
max=i;
}
swap(arr,min,left);
if(max==left)
max=min;
swap(arr,right,max);
left++;
right--;
}
}
三、插入排序
插入排序就是向有序区间内将无序区间的元素依次插入,加入某个数组的长度为n,那么初始的有序区间为[0,1) ,无序区间为[1,n-1)
插入排序在对相对有序的数据进行排序时效率是很高的!
public void insertSort(int[] arr){
//有序区间为[0,i)
for(int i=1;i<arr.length;i++){
//无序区间为[i,arr.length-1]
//当无序区间的第一个元素小于有序区间最后一个元素时,交换两个元素
for (int j = i; j >0 && arr[j]<arr[j-1]; j--) {
swap(arr,j,j-1);
}
}
}
当只有最后一个元素都是有序时,时间复杂度为O(n),但这种情况很少,基本都是在O(n)-O(n^2)之间;
由于是一个个往前挪,当相等时不交换,也就不会改变相同数的相对位置,所以该排序是稳定的。
3.2插入排序的改进
上面我们写的将元素依次往前挪的过程实际就是查找到该元素应该放在哪个位置。再移动元素,移动元素的次数是不可能变化的,但是我们可以用二分查找提高查找效率。
public void insertSortImprove(int[] arr){
int left=0;
int right=0;
int mid=0;
for (int i = 1; i < arr.length; i++) {
left=0;
right=i;
int val=arr[i];
while (left < right) {
mid=left+((right-left)>>1);
if (arr[i] >= arr[mid]) {
left=mid+1;
}else {
//取不到right,所以不用-1
right=mid;
}
}
for (int j = i; j >left; j--) {
arr[j]=arr[j-1];
}
arr[left]=val;
}
}
四、希尔排序
希尔排序实质上是一种分组插入排序,先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;之后又取第二个增量直到数组排序完成。增量一般选取数组长度n依次除2或者除3.
public void shellSort(int[] arr){
int gap=arr.length>>1;
while (gap > 1) {
insertSortGap(arr,gap);
gap/=2;
}
insertSortGap(arr,1);
}
private void insertSortGap(int[] arr, int gap) {
for (int i = gap; i <arr.length; i++) {
for (int j = i; j-gap>=0 && arr[j]<arr[j-gap] ; j=j-gap) {
swap(arr,j,j-gap);
}
}
}
希尔排序的平均时间复杂度为O(n^1.3),最坏情况是O(n ^2)
由于多次的插入排序会导致相同元素的相对顺序变化,所以该排序算法是不稳定的。
五、堆排序
堆排序的思想就是先建立一个最大堆,然后取出最大元素(也就是数组第一个元素)与最后一个元素交换,此时最后一个元素有序,调整堆后,将最大元素与无序区间最后一个元素交换,重复此操作直到数组全部有序。
public void heapSort(int[] arr){
for (int i = (arr.length-1-1)/2; i >=0 ; i--) {
shiftDown(arr,i,arr.length-1);
}
for (int i = arr.length-1; i >0; i--) {
swap(arr,0,i);
shiftDown(arr,0,i);
}
}
//进行堆的调整
private void shiftDown(int[] arr,int i,int n){
while (i * 2 + 1 < n) {
int j=i*2+1;
if(j+1<n&&arr[j]<arr[j+1]){
j++;
}
if(arr[i]<arr[j]){
swap(arr,i,j);
}
i=j;
}
}
由于需要交换n次,调整堆的时间复杂度为O(logn),所以堆排序的时间复杂度为O(nlogn);
由于在不断调整堆时相同的数的相对顺序可能会改变,所以该排序算法是不稳定的。
六、归并排序
归并排序是分治法的经典运用,该算法将元素不断的两两相分,再两两相结合排序。
private void mergeSortInternal(int[] arr,int l,int r){
if(r-l<1)
return;
int mid=l+((r-l)>>1);
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid+1,r);
merge(arr,l,mid,r);
}
private void merge(int[] arr, int i,int mid, int r) {
int[] nums=new int[r-i+1];
for (int j = i; j <=r ; j++) {
nums[j-i]=arr[i];
}
int m=i;
int n=mid+1;
for (int j = i; j <= r; j++) {
if (j > m) {
arr[j]=nums[n-j];
n++;
} else if (j > n) {
arr[j]=nums[m-j];
}else if(nums[m-i]<nums[n-i]){
arr[j]=nums[m-i];
m++;
}else {
arr[j]=nums[n-i];
n++;
}
}
}
归并排序遍历整个大数组的时间复杂度为O(n),不断的拆分数组,由二叉树的深度可得时间复杂度为O(logn),总共的时间复杂度为O(nlogn)。(但是由于额外创建了数组,空间复杂度为O(n))
在merge操作中,当两个元素相同时,可以先把左边的值放入,这就保证了相对位置,所以该排序是稳定的。
6.1归并排序的优化
优化一:当元素个数小于15个时,可以使用插入排序进行排序
优化二:只有当左边的第一个元素大于右边的第一个元素时,才需要进行merge操作
public void mergeSort(int[] arr){
mergeSortInternal(arr,0,arr.length-1);
}
private void mergeSortInternal(int[] arr,int l,int r){
if(r-l<15){
insertSortSetion(arr,l,r);
return;
}
int mid=l+((r-l)>>1);
mergeSortInternal(arr,l,mid);
mergeSortInternal(arr,mid+1,r);
if(arr[l]>arr[r])
merge(arr,l,mid,r);
}
private void insertSortSetion(int[] arr, int l, int r) {
for (int i = l+1; i <=r ; i++) {
for (int j = i; j >l && arr[j]<arr[j-1] ; j--) {
swap(arr,j,j-1);
}
}
}
private void merge(int[] arr, int i,int mid, int r) {
int[] nums=new int[r-i+1];
for (int j = i; j <=r ; j++) {
nums[j-i]=arr[i];
}
int m=i;
int n=mid+1;
for (int j = i; j <= r; j++) {
if (j > m) {
arr[j]=nums[n-j];
n++;
} else if (j > n) {
arr[j]=nums[m-j];
}else if(nums[m-i]<nums[n-i]){
arr[j]=nums[m-i];
m++;
}else {
arr[j]=nums[n-i];
n++;
}
}
}
6.2 归并排序的非递归写法
public void mergeSortNoRecursion(int[] arr){
for (int i = 1; i <=arr.length ; i*=2) {
for (int j = 0; j+i < arr.length; j=j+2*i) {
//右边的区间可能会超过数组长度,所以需要取最小值
merge(arr,j,j+i-1,Math.min(arr.length-1,j+2*i-1));
}
}
}
七、快速排序
快速排序就是选择一个基准值V,然后将所有小于V的值放在V的左边,将所有大于V的值放在右边,通过对分区间的整合,最终实现排序。
public void quickSort(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
private void quickSortInternal(int[] arr, int l, int r) {
if(r-l<15){
insertSortSetion(arr,l,r);
return;
}
int p=partation(arr,l,r);
quickSortInternal(arr,l,p-1);
quickSortInternal(arr,p+1,r);
}
private int partation(int[] arr, int l, int r) {
int v=arr[l];
int j=l;
for (int i = l+1; i <=r; i++) {
if(arr[i]<v){
swap(arr,i,j+1);
j++;
}
}
swap(arr,l,j);
return j;
}
快排时间复杂度为O(nlogn);
由于在分区时元素相对位置可能会乱,所以是不稳定的。
7.1快速排序优化
方法一:由于选取基准值时默认选的都是第一个,有可能会导致分区链式化,所以选取时可以选择随机基准值。
方法二:当排序区间小于15时,使用选择排序可以提高性能。
但是当出现极端情况,也就是所有元素都相同或者大量元素相同时,快排时间复杂度就会退化成O(N^2),这时需要使用双路快排或三路快排
7.2双路快速排序
双路快排就是在快速排序的基础上,分别从数组左右两端向中间扫描,让小于等于V的数放在数组左边,大于等于数组的数放在数组右边,从而实现相同的数均匀分布,就不会出现有大量元素时只有左端进行排序或者右端进行排序这种情况。
public void quickSort2(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
private void quickSortInternal2(int[] arr, int l, int r) {
if(r-l<15){
insertSortSetion(arr,l,r);
return;
}
int p=partation(arr,l,r);
quickSortInternal(arr,l,p-1);
quickSortInternal(arr,p+1,r);
}
private int partation2(int[] arr, int l, int r) {
int v=arr[l];
int j=r;
int i=l+1;
while (true) {
while (arr[i]<=v){
i++;
}
while (arr[j] >= v) {
j++;
}
if (i > j) {
break;
}
swap(arr,i,j);
}
swap(arr,l,j);
return j;
}
7.3三路快排
三路快排就是一次性将相等的元素区间固定在中间位置,只需递归快排左边小于V的元素和右边大于V的元素,这样效率进一步提高。
public void quickSort3(int[] arr){
quickSortInternal(arr,0,arr.length-1);
}
private void quickSortInternal3(int[] arr, int l, int r) {
if(r-l<15){
insertSortSetion(arr,l,r);
return;
}
int v=arr[l];
int i=l;
int mid=l+1;
int j=r+1;
while (mid < j) {
if (arr[i] < v) {
swap(arr,mid+1,i);
i++;
mid++;
} else if (arr[i] > v) {
swap(arr,j-1,i);
j--;
}
}
quickSortInternal3(arr,l,i);
quickSortInternal3(arr,mid,r);
}