一. 七种常见的排序
1.插入排序
思想:将数组拆分成两段,一段有序,一段无序,将无序区间中的数依次拆入到有序区间中。示例图:
实现代码:
public void insertSort(int[] array) {
//i 循环控制插入次数及要插入的数
for(int i = 0;i < array.length-1; i++) {
//temp = 要拆入的数,从下标为1的位置开始是因为一个数的时候自然有序
int temp = array[i+1];
//在有序数组中找出要插入的位置(从后往前遍历有利于对有序数组进行插入操作)
int j = i;
for (; j >= 0; j--) {
//如果要插入的数小于当前数 令当前数后移一位 否则退出循环(既找到了要拆入的位置为array[j]的后一位)
if(temp < array[j]) {
array[j+1] = array[j];
}else {
break;
}
}
//将数插入
array[j + 1] = temp;
}
}
时间和空间复杂度及稳定性:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为后一个数要是和前面的相等之间插入前一个数后面,所以具有稳定性
2.冒泡排序
思想:从一个数开始比较找到最大的放到数组的最后一个位置,在对剩下的数继续比较将最大的数放在倒数第二位,依次类推直到全部有序。示例图:
然后在对蓝色区间进行上述操作:
直到数组有序
实现代码:
public void bubbleSort(int[] array) {
//i循环控制循环次数
for (int i = 0; i < array.length; i++) {
//每次j循环找出最大的数放在最后面,下次循环可以少比较一次
for (int j = 0; j < array.length - i - 1; j++) {
//比较两个数,将大的数放在后面,以保证可以把最大的数放在最后一个位置
if(array[j+1] < array[j]) {
int t = array[j+1];
array[j+1] = array[j];
array[j] = t;
}
}
}
}
时间复杂度,空间复杂度和稳定性
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为只有大小不等的时候才进行交换,所以具有稳定性
3.快速排序
思想:找到一个数将数组分为两部分,左边小于这个数,右边大于这个数,(有点像搜索树),然后对左右两边的得到的数组继续进行拆分,直到数字中只有一个数或者没有数(分而治之)。示例图:
此时数组类的数所在的位置就是图中的的关系(树的关系)
实现代码:
public void quickSort(int[] array) {
quickSortInternal(array,0,array.length-1);
}
//这个方法是为了递归实现更换了原方法的参数列表
public void quickSortInternal(int[] array, int lowIndex, int highIndex) {
//如果数组长度 <= 1 自然有序
//因为时左闭右闭 所以数组长度为 rightIndex - leftIndex + 1
if(highIndex - lowIndex + 1 <= 1) {
return;
}
//找到中间位置的下标
int midIndex = partition(array,lowIndex,highIndex);
//对数组左边再次进行快排([lowIndex,midIndex-1])
quickSortInternal(array,lowIndex,midIndex-1);
//对数组右边再次进行快排([midIndex+1,highIndex])
quickSortInternal(array,midIndex +1,highIndex);
}
public int partition(int[] array,int lowIndex,int highIndex){
//令第一个数为中间数(这里为了方便实现,实际中可以对要选的中间数进行优化)
int key = array[lowIndex];
int leftIndex = lowIndex;
int rigthIndex = highIndex;
while(leftIndex < rigthIndex) {
//找到右边小于中间数 的数
while(leftIndex < rigthIndex && array[rigthIndex] >= key) {
rigthIndex--;
}
//找到左边大于中间数 的数
while(leftIndex < rigthIndex && array[leftIndex] <= key) {
leftIndex++;
}
//交换这两个数
int temp = array[leftIndex];
array[leftIndex] = array[rigthIndex];
array[rigthIndex] = temp;
}
//到这里时除了第一个数(确定的中间数)其他数都有序了,且leftIndex=rightIndex 为小于或等于中间数的位置下标
//将第一个数和下表为leftIndex位置的数进行交换
int temp = array[lowIndex];
array[lowIndex] = array[leftIndex];
array[leftIndex] = temp;
//返回中间下标
return leftIndex;
}
时间复杂度,空间复杂度,稳定性:
时间复杂度:每次都要划分数组划分次数为logn(搜索树的高度),每次都要进行遍历拆分的数组,数组内元素个数为n,因此时间复杂度为:O(nlogn)
空间复杂度:开辟了常数个单元(拆分的时候开辟的单元),所以空间复杂度为:O(logn)
稳定性:因为要和前面的数进行交换,两个数一样的话会改变位置,所以不具备稳定性
4.归并排序
思想:将数组从中间拆分为两个数组,分别使这两个数组有序,然后继续拆分,直到不能在拆分了合并两个数组,依次合并两个有序数组,直到整个数组有序(先拆再和)。示例图:
实现代码:
public void mergeSort(int[] array) {
mergeSortInternal(array,0,array.length);
}
//这个方法是为了递归实现更换了原方法的参数列表
public void mergeSortInternal(int[] array,int leftIdnex,int rightIndex) {
//如果数组长度 <= 1 既数组中只右一个数 自然有序
//因为时左闭右开 所以数组长度为 rightIndex - leftIndex
if(rightIndex - leftIdnex <= 1) {
return ;
}
//找到要进行拆分的位置
int midIndex = (rightIndex + leftIdnex)/2;
//对左边进行归并
mergeSortInternal(array,leftIdnex,midIndex);
//对右边进行归并
mergeSortInternal(array,midIndex,rightIndex);
//合并两个有序区间
merge(array,leftIdnex,midIndex,rightIndex);
}
public void merge(int[] array,int leftIndex,int midIndex,int rightIndex) {
//定义一个需要合并的临时数组
int[] mergeArray = new int[rightIndex - leftIndex];
int i = leftIndex;
int j = midIndex;
int t = 0;
//当两个数组都没有遍历完时,对他们进行进行合并(放到临时数组中)
while(i < midIndex && j < rightIndex) {
if(array[i] <= array[j]) {
mergeArray[t++] = array[i++];
}else {
mergeArray[t++] = array[j++];
}
}
//有一个数组以及遍历完,将剩下的数放入临时数组中
while(i < midIndex) {
mergeArray[t++] = array[i++];
}
while(j < rightIndex) {
mergeArray[t++] = array[j++];
}
//把临时数组中的数按位置搬到原数组中
for (int value : mergeArray) {
array[leftIndex++] = value;
}
}
时间复杂度,空间复杂度和稳定性
时间复杂度:要先拆,且是从中间拆,要拆logn次,每次合并都要遍历数组,所以时间复杂度是O(nlongn)
空间复杂度:每次合并都要开辟新的空间,合并log2n次,每次拆分新的数组都会利用一个空间,所以要用n个空间,所以空间复杂度为:O(n)
稳定性:因为交换元素时,可以在相等的情况下做出不移动的限制,所以归并排序是可以稳定的
5.堆排序
思想:将数组构建成一个小堆,每次取出堆顶元素,然后把最后面的元素放的堆顶,进行向下调整
实现代码:参考java 的优先级队列(priorityQueue)
时间复杂度,空间复杂度,稳定性
时间复杂度:因为是堆的数据结构实现,所以时间复杂度为O(nlogn)
空间复杂度:因为是就地排序不开辟新的空间所以空间复杂度为:O(1)
稳定性:建队过程破坏数组顺序,所以不稳定
6.直接选择排序:
思想:从数组中找出最小的,放在第一位,然后从剩下的数据中再找出最小的放到第二位依次类推
实现代码:
public void SelectSort(int[] arr){
for(int i = 0;i<arr.length-1;i++){
for(int j = i+1;j<arr.length;j++){
if(arr[i] <= arr[j])
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
时间复杂度,空间复杂度和稳定性:
时间复杂度:O(n²)
空间复杂度:O(1)
稳定性:因为是选择最小的或者等于依次放在前面,会改变相等的元素的位置关系,所以不稳定
7.希尔排序
思路:根据增量序列的值ti,每趟排序会把初始序列划分成若干个元素的子序列,然后对这些子序列使用插入排序,因为这是递减增量序列,所以第一趟的排序,增量值最大,那么划分的子序列数量也就最多,每个子序列的元素也就越少,可以看做是一个“几乎”已经排好序的序列,当增量值越来越小的时候,子序列数量也就越少,每个子序列的元素也就越多,但是,基于前几次的排序,这些子序列虽然元素多,但是已经是一个“几乎”排好序的序列了,当最后一次排序的时候,即增量序列的值为1时,就是对整个无序序列进行排序,这时整个序列已经是一个“几乎”排好序的序列了。
实现代码:
public void shellSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int len = arr.length;
// 首先选取一个增量序列
int gap = 1;
while (gap < len) {
// 先找出增量序列最大的增量值,也就是将序列分为gap组
gap = gap * 3 + 1;
}
while (gap > 0) {
for (int i = gap; i < len; i++) {
int tmp = arr[i];
int j = i - gap;
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
gap = (int) Math.floor(gap / 3);
}
时间复杂度,空间复杂度,稳定性
时间复杂度:效率依赖于递减增量序列的选择,时间复杂度最坏的情况是O(nlogn)。
空间复杂度:没有开辟新空间(开辟了常数个空间),所以空间复杂度为:O(1)
稳定性:因为要选择比较区间会破坏稳定性,所以不稳定性
表格总结:
具有稳定性的排序:冒泡,选择,归并
平均时间复杂度为O(nlogn):希尔,快排,归并,堆排
空间复杂度不为O(1):快排(O(logn)),归并(O(n))