我们常用的排序算法有插入排序、选择排序、交换排序和归并排序四种算法,这四种算法各自有各自的优缺点,接下来我们就仔细来看看这几种算法。
代码展示:
public void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= 0 ; j--) {
if(array[j] > tmp){
array[j+1] = array[j];
}else {
//j回退的时候,遇到了比tmp小的元素就结束这次的比较
//array[j+1] = tmp;因为最外面也有这一步,因此这里省略
break;
}
}
//有下面这一步是因为j回退到了小于0的位置的地方,上面的for循环进不去
array[j+1] = tmp;
}
}
时间复杂度:O(N^2)即逆序的时候
最好的情况是O(N):对于直接插入排序来说,最好的情况就是数据有序
根据这个结论,推导出另一个结论,对于直接插入排序来说,数据越有序,排序速度越快
空间复杂度:O(1)
稳定性:稳定的 ,一个稳定的排序可以实现为不稳定的排序 ,但是一个本身就不稳定的排序,是不可能变成稳定的排序的。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
public void shell(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
int j = i - gap;
for (; j >= 0; j-=gap) {
if(array[j] > tmp){
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap] = tmp;
}
}
//分组
public void shellSort(int[] array){
int gap = array.length;
while (gap > 1){
shell(array,gap);
gap /= 2;
}
shell(array,1);//保证最后是一组
}
其实就是一个直接插入排序
array:待排序序列 gap:组数
时间复杂度是:O(N^1.3 - N^1.5)
空间复杂度:O(1)
稳定性:不稳定 看是否稳定主要是看在比较的过程中,如果发生了跳跃式的交换,那么就是不稳定的排序
选择排序就是一组数据中,相邻两个数,互相比较,大的放到后面,小的放到前面。
我们先写一个交换函数:
public void swap(int[] array,int i,int j){
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
public void selectSort1(int[] array){
for (int i = 0; i < array.length; i++) {
int j = i+1;
for (; j < array.length; j++) {
if(array[j] < array[i]){
swap( array,i,j);
}
}
}
}
但是这个代码我们还有优化得地方,我们不必要每个后面小得值都和前面交换,可以在后面找到最小值得下标,然后和前面的进行交换
public void selectSort(int[] array){
for (int i = 0; i < array.length; i++) {
int minindex = i;
for (int j = i + 1; j < array.length; j++) {
//找到最小值下标
if(array[j] < array[minindex]){
minindex = j;
}
}
swap(array,i,minindex);
}
}
稳定性:不稳定
时间复杂度:O(n^2)
空间复杂度:O(1)
public void heapSort(int[] array){
createHeap(array);
int end = array.length-1;
while (end > 0){
swap(array,0,end);
shiftDown(array,0,end);
end--;
}
}
public void createHeap(int[] array){
for (int parent = (array.length-1-1)/2; parent >= 0 ; parent--) {
shiftDown(array,parent,array.length);
}
}
public void shiftDown(int[] array,int parent,int len){
int child = 2*parent+1;
while (child < len){
//这里条件child + 1 < len,是因为怕最后一棵树没有右孩子导致数组越界
if(child+1 < len && array[child] < array[child+1]){
//这里的child保证一定是孩子的最大值下标
child++;
}
if(array[child] > array[parent]){
swap(array,child,parent);
//继续向下检测,防止有的父亲节点<孩子节点
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
时间复杂度 = O(n * log n)
空间复杂度 = O(1)
稳定性:不稳定
优化前:
public void bubbleSort1(int[] array){
//i表示趟数
for (int i = 0; i < array.length-1; i++) {
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]){
swap(array,j,j+1);
}
}
}
}
优化后:
public void bubbleSort2(int[] array){
//i表示趟数
for (int i = 0; i < array.length-1; i++) {
boolean flg = false;
for (int j = 0; j < array.length-1-i; j++) {
if(array[j] > array[j+1]){
swap(array,j,j+1);
flg = true;
}
}
if(flg == false){
break;
}
}
}
原理:(1) 从待排序区间选择一个数,作为基准值(pivot)
(2)Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可 以包含相等的)放到基准值的右边
(3)采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间 的长度 == 0,代表没有数据
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
//递归左边和右边
public void quick(int[] array,int left,int right){
//left和right相遇时,递归结束
if(left >= right){
return;
}
int pivot = partition(array,left,right);//找基准
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
//找基准
private int partition(int[] array,int start,int end){
int tmp = array[start];
while (start < end){
while (start < end && array[end] >= tmp){
end--;
}
//end下标就遇到了 < tmp的值
array[start] = array[end];
while (start < end && array[start] <= tmp){
start++;
}
//start下标就遇到了 > tmp的值
array[end] = array[start];
}
array[start] = tmp;
return start;
}
时间复杂度:最好的情况(每次都可以均匀的分割待排序序列)O(N * log 2 n)
最坏的情况:数据有序或者逆序的情况下O(N^2)
空间复杂度: 最好的情况下:O(logn)
最坏的情况下:O(n) 成为单分支的一棵树
稳定性:不稳定的排序
可是我们仔细思考一下上面的代码还能再优化吗?
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
//递归左边和右边
public void quick(int[] array,int left,int right){
//left和right相遇时,递归结束
if(left >= right){
return;
}
//1.找基准之前,我们找到中间大小的值-使用三数取中法
int midValIndex = findMidValIndex(array,left,right);
swap(array,midValIndex,left);
int pivot = partition(array,left,right);//找基准
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
//找基准
private int partition(int[] array,int start,int end){
int tmp = array[start];
while (start < end){
while (start < end && array[end] >= tmp){
end--;
}
//end下标就遇到了 < tmp的值
array[start] = array[end];
while (start < end && array[start] <= tmp){
start++;
}
//start下标就遇到了 > tmp的值
array[end] = array[start];
}
array[start] = tmp;
return start;
}
继承上面的优化,我们排序一组数据,如果我们找到一个基准,在这组数据中,有和这个基准相同的数据,我们就把这个数据放到这个基准的旁边,直接递归剩下的数据就行。
或者我们的待排序数据小于一个给定的数据总数时,我们可以采取直接插入排序就行。
public void quickSort(int[] array){
quick(array,0,array.length-1);
}
public void insertSort3(int[] array,int start,int end){
for (int i = 1; i <= end; i++) {
int tmp = array[i];
int j = i-1;
for (; j >= start ; j--) {
if(array[j] > tmp){
array[j+1] = array[j];
}else {
//j回退的时候,遇到了比tmp小的元素就结束这次的比较
//array[j+1] = tmp;因为最外面也有这一步,因此这里省略
break;
}
}
//有下面这一步是因为j回退到了小于0的位置的地方,上面的for循环进不去
array[j+1] = tmp;
}
}
//递归左边和右边
public void quick(int[] array,int left,int right){
//left和right相遇时,递归结束
if(left >= right){
return;
}
//如果区间内的数据,在排序的过程当中,小于某个范围了,可以直接使用插入排序
if(right - left+1 <= 140){
//使用直接插入排序
insertSort3(array,left,right);
}
//1.找基准之前,我们找到中间大小的值-使用三数取中法
int midValIndex = findMidValIndex(array,left,right);
swap(array,midValIndex,left);
int pivot = partition(array,left,right);//找基准
quick(array,left,pivot-1);
quick(array,pivot+1,right);
}
//找基准
private int partition(int[] array,int start,int end){
int tmp = array[start];
while (start < end){
while (start < end && array[end] >= tmp){
end--;
}
//end下标就遇到了 < tmp的值
array[start] = array[end];
while (start < end && array[start] <= tmp){
start++;
}
//start下标就遇到了 > tmp的值
array[end] = array[start];
}
array[start] = tmp;
return start;
}
代码实现
public void quickSort(int[] array){
Stack stack = new Stack<>();
int left = 0;
int right = array.length-1;
int pivot = partition(array,left,right);
//左边有两个元素
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(array,left,right);
//左边有两个元素
if(pivot > left+1){
stack.push(left);
stack.push(pivot-1);
}
//右边有两个元素
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
}
}
在进行归并排序之前,我们先来看一个合并两个有序数组为一个有序数组
代码展示:
public int[] mergeArray(int[] array1, int[] array2) {
int i = 0;//表示tmp数组的下标i
int s1 = 0;
int e1 = array1.length - 1;
int s2 = 0;
int e2 = array2.length - 1;
int[] tmp = new int[array1.length + array2.length];
while (s1 <= e1 && s2 <= e2) {
if (array1[s1] <= array2[s2]) {
tmp[i++] = array1[s1++];
//i++;
//s1++;
} else {
tmp[i++] = array2[s2++];
//i++;
//s2++;
}
}
while (s1 <= e1) {
tmp[i++] = array1[s1++];
}
while (s2 <= e2) {
tmp[i++] = array2[s2++];
}
return tmp;
}
代码展示:
public void mergeSort(int[] array){
mergeSortInternal(array,0,array.length-1);
}
private void mergeSortInternal(int[] array,int low,int high){
if(low >= high){
return;
}
int mid = low + ((high - low) >>> 1);
//左边
mergeSortInternal(array,low,mid);
//右边
mergeSortInternal(array,mid+1,high);
//合并
merge(array,low,mid,high);
}
private void merge(int[] array,int low,int mid,int high){
int[] tmp = new int[high-low+1];
int i = 0;//表示tmp数组的下标i
int s1 = low;
int e1 = mid;
int s2 = mid+1;
int e2 = high;
while (s1 <= e1 && s2 <= e2) {
if (array[s1] <= array[s2]) {
tmp[i++] = array[s1++];
//i++;
//s1++;
} else {
tmp[i++] = array[s2++];
//i++;
//s2++;
}
}
while (s1 <= e1) {
tmp[i++] = array[s1++];
}
while (s2 <= e2) {
tmp[i++] = array[s2++];
}
//拷贝tmp数组的元素,放入原来的数组array中
for (int j = 0; j < tmp.length; j++) {
array[j+low] = tmp[j];
}
}
结论:归并排序 时间复杂度:O(N*logN) 时间复杂度是O(N*logN)的一共有快排、归并和堆排,三个里面快排最快,堆排空间复杂度最低,归并最稳定 空间复杂度:O(N) 稳定性:稳定的排序 如果array[s1] <= array[s2] 不去等号,那么就是不稳定的排序 目前为止,我们学过的排序只有冒泡、插入、归并是稳定的。
public void mergeSort2(int[] array){
int nums = 1;//每组的数据个数
while (nums < array.length){
//数组每次都要从i进行遍历,确定要归并的区间
for (int i = 0; i < array.length; i += nums*2) {
int left = i;
int mid = left+nums-1;
//防止越界
if(mid >= array.length){
mid = array.length-1;
}
int right = mid + nums;
//防止越界
if(right >= array.length){
right = array.length-1;
}
//下标确定之后,进行合并
merge(array,left,mid,right);
}
nums *= 2;
}
}
public void countingSort(int[] array){
//假设开始最大和最小
int maxVal = array[0];
int minVal = array[0];
for(int i = 1;i < array.length;i++){
if(array[i] < minVal){
minVal = array[i];
}
if(array[i] > maxVal){
maxVal = array[i];
}
}
//说明已经找到最小值和最大值
int[] count = new int[maxVal-minVal+1];//计数数组默认都是0
//统计array数组当中,每个数据出现的次数
for(int i = 0;i < array.length;i++ ){
int index = array[i];
//为了空间得合理化使用,这里需要index-minVal,防止945-900
count[index-minVal]++;
}
//说明在计数数组当中,已经把array数组中,每个数据出现的次数都已经算好了
//接下来,只需要遍历计数数组,把数据写回array
int indexArray = 0;
for (int i = 0; i < count.length; i++) {
while (count[i] > 0){
//这里一定要加minVal,因为不一定就是i出现了count[i]
array[indexArray] =i+minVal;
count[i]--;//拷贝一个之后,次数也就减少一个
indexArray++;//下标得向后移动
}
}
}
计数排序,一般适用于 有n个数,数据范围在0-n之间的数
时间复杂度:O(N)
空间复杂度:O(M)M代表当前数据的范围,900-999,范围就是100
稳定性:当前代码不稳定,