文章目录
- 一、直接插入排序
- 二、希尔排序
- 三、直接选择排序
- 四、堆排序
- 五、冒泡排序
- 六、快速排序
- 6.1递归实现快速排序
- 6.2非递归实现快速排序
- 七、归并排序
- 7.1递归实现归并排序
- 7.2非递归实现归并排序
- 八、计数排序
以下排序以从小到大排序为例
一、直接插入排序
时间复杂度:
最好情况:完全有序的情况 1 2 3 4 5 O(N)
最坏情况:完全逆序的情况 5 4 3 2 1 O(N^2)(相当于等差数列求和)
空间复杂度:O(1)
稳定性:稳定
当所给的数据越有序,直接插入排序越快
有一组基本有序的数据时,用直接插入排序较好
public static void insertSort(int[] array) {
for(int i = 1; i < array.length; i++) {
int j = i - 1;
int tmp = array[i];
for(; j >= 0; j--) {//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
二、希尔排序
希尔排序是对直接插入排序的优化,跳跃式的分组可能会将更小的元素尽可能往前方
增量为多少(gap为多少),则被分为多少组
时间复杂度:N^1.3 ~ N^1.5
空间复杂度:O(1)
稳定性:不稳定
public static void shellSort(int[] array) {
int gap = array.length;//增量为多少(gap为多少),则被分为多少组
while(gap > 1) {//里面已经包括了排序gap为1的情况
gap /= 2;
shell(array, gap);
}
}
private static void shell(int[] array, int gap) {
for(int i = gap; i < array.length; i++) {//i+=gap也行,因为gap最后会变为1
int j = i - gap;
int tmp = array[i];
for(; j >= 0; j -= gap) {//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + gap] = array[j];
}else {
break;
}
}
array[j + gap] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
三、直接选择排序
时间复杂度:不管情况是好还是坏,下面两种写法都是O(N^2)(相当于等差数列求和)
空间复杂度:O(1)
稳定性:不稳定
public static void selectSort1(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;//在所有的j下标元素中找比array[minIndex]还要小的元素的下标
}
}
swap(array, i, minIndex);
}
}
private static void swap(int[]array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
//写法二
public static void selectSort2(int[] array){
int left = 0;
int right = array.length - 1;
while(left < right) {//与写法一相比将写法一最外层的for循环改为了while循环
int minIndex = left;
int maxIndex = left;//maxIndex一定是left,因为下面的j下标是从left+1开始从前往后遍历
for(int i = left + 1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;//在所有的j下标元素中找比array[minIndex]还要小的元素的下标
}
if(array[i] > array[maxIndex]) {
maxIndex = i;//在所有的j下标元素中找比array[maxIndex]还要大的元素的下标
}
}
swap(array, left, minIndex);
//当最大值原来刚好在最小值的位置(left位置)时,则上一步已经将最小值与left交换,此时最大值在原来最小值的位置,
if(maxIndex == left) {//所以要做这一步操作
maxIndex = minIndex;
}
swap(array, right, maxIndex);
left--;
right++;
}
}
四、堆排序
时间复杂度:O(N)(创建大根堆)+ O(NlogN) (堆的每个节点进行向下调整) 即O(NlogN)
空间复杂度:O(1)
稳定性:不稳定
数据量非常大的时候,堆排序一定比希尔排序快,因为希尔排序的时间复杂度为N^1.3 ~ N^1.4,堆排是对数希尔排是指数
public static void heapSort(int[] array){
creatBigHeap(array);
int end = array.length - 1;
while(end > 0) {
swap(array, 0, end);//因为是大堆,堆顶的元素最大,将堆顶元素与队尾元素交换,使队尾元素变成队内最大元素
shiftDown(array, 0, end);//在这里end=array.length-1,而在shiftDown中end代表数组的总长度,
end--; //相当于去掉队尾元素再进行向下调整
}
}
private static void creatBigHeap(int[] array) {
for(int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array, parent, array.length);
}
}
private static void shiftDown(int[] array, int parent, int end) {
int child = 2 * parent + 1;
while(child < end) {//因为end传的是array.length,所以条件用<而不用<=
if(child + 1 < end && array[child] < array[child + 1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
}else {
break;
}
}
}
五、冒泡排序
时间复杂度:O(N^2) 加了优化之后,最好的情况(只比较一趟)则是O(N)
空间复杂度:O(1)
稳定性:稳定
public static void bubbleSort(int[] array){
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) {
return;
}
}
}
六、快速排序
快速排序(相当于以基准为根创建二叉树)
时间复杂度:
最好情况:O(N*logN) 满二叉树/完全二叉树
最坏情况:O(N^2)(相当于等差数列求和) 单分支的树
空间复杂度:
最好情况:O(logN) 满二叉树/完全二叉树的高度
最坏情况:O(N) 单分支的树创建N个节点
稳定性:不稳定
三种求基准的方法:Hoare法,挖坑法,前后指针法
6.1递归实现快速排序
在这里递归实现的快速排序做了两个优化,第一个优化是使用三数取中法求原始基准,使创建出来的树更像满二叉树,降低了树的高度,降低了空间复杂度
第二个优化是递归到一定程度时对每个区间使用插入排序,因为区间越来越小且区间的内容越来越有序,这时这部分区间可以用插入排序以减少递归次数
public static void quickSort(int[] array){
quickSortF(array, 0, array.length - 1);//参数right传的是array.length-1
}
private static void quickSortF(int[] array, int left, int right) {
if(left >= right) return;//这是递归的结束条件,有=,因为left=right时不用再创建节点了,这个节点已经有序
//递归到后面的较小区间时用插入法
if(right - left + 1 <= 7) {//随着快速排序的进行,整个数据正在趋于有序,当区间越来越小且区间的内容越来越有序时,
insertSort1(array, left, right);//这部分区间可以用插入排序,这样做可以减少递归的次数
return;
}
//三数取中法,降低了二叉树的高度,降低了空间复杂度,使空间复杂度变为O(logn)
int midIndex = midOfTree(array, left, right);
swap(array, midIndex, left);//交换完之后保证left下标是三个数中中间大的数字
int pivot = partition2(array, left, right);//找到一次基准相当于排好了当前基准这个元素在数组中的顺序,找到基准后,基准左边都是比基准小的,基准右边都是比基准大的
//先递归左边,递归完左边再递归右边
quickSortF(array, left, pivot - 1);
quickSortF(array, pivot + 1, right);
}
//插入法
private static void insertSort1(int[] array, int left, int right) {
for(int i = left + 1; i <= right; i++) {
int j = i - 1;
int tmp = array[i];
for(; j >= left; j--) {//跳出循环有两种情况,1.j<0 2.array[j]<=tmp
if(array[j] > tmp) {//如果条件变为array[j]>=tmp,则排序变为不稳定
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = tmp;//每次把tmp插入数组后,都会重新获取i和j的位置
}
}
//在三数中找到中间大小数的下标
private static int midOfTree(int[] array, int left, int right) {
int mid = (left + right) / 2;
if(array[left] < array[right]) {
if(array[mid] < array[left]) {
return left;
}else if(array[mid] > array[right]) {
return right;
}else {
return mid;
}
}else {
if(array[mid] > array[left]) {
return left;
}else if(array[mid] < array[right]) {
return right;
}else {
return mid;
}
}
}
//三种找基准的方法,在排序的过程中序列可能会不一样,但最终排好序的结果一样,建议优先使用挖坑法,right参数传的都是array.length-1
//Hoare法找基准
private static int partition1(int[] array, int left, int right) {
int key = array[left];
int i = left;//将基准的下标保存到i中,因为后面要用left与right相交位置的元素与基准进行交换,这里的交换函数只传下标
while(left < right) {
while (left < right && array[right] >= key) {//left<right这个条件很重要,因为right不断--,此时再判断array[right]>=key时array[right]有可能会越界
right--;//先走right,因为一开始基准为left,先走right,left和right相遇的地方才能是比基准小的,才能把比基准小的与基准交换放在基准前
}
while (left < right && array[left] <= key) {//array[left]<=key或上面array[right]>=key的=一定要有,否则可能会进入死循环(如当left和right的值相同时)
left++;
}
swap(array, left, right);
}
swap(array, left, i);
return left;
}
//挖坑法找基准
private static int partition2(int[] array, int left, int right) {
int key = array[left];
while(left < right) {
while (left < right && array[right] >= key) {
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key) {
left++;
}
array[right] = array[left];
}
array[left] = key;
return left;
}
//前后指针法找基准
private static int partition3(int[] array, int left, int right) {
int prev = left;
int cur = left + 1;
while(cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[cur]) {//cur在前面找比基准小的,prev保持在cur找到比基准小的之后要跟prev交换的那个位置,prev是前置++
swap(array, prev, cur);//代码走到这说明cur和prev拉开了距离且cur<left,prev>=left
}
cur++;//array[cur]>=array[left]时cur直接往前走,与prev拉开距离
}
swap(array, prev, left);
return prev;
}
6.2非递归实现快速排序
public static void quickSortNor(int[] array) {
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = array.length - 1;
int piovt = partition2(array, left, right);
if(piovt - 1 > left) {
stack.push(left);//往栈上先放left后放right,则出栈时先出right后出left
stack.push(piovt - 1);
}
if(piovt + 1 < right) {
stack.push(piovt + 1);
stack.push(right);
}
while(!stack.isEmpty()) {//因为这里出栈时先出right后出left,根据出出来的right和left找新的基准,所以相当于先递归二叉树右边,再递归二叉树左边
right = stack.pop();
left = stack.pop();
piovt = partition2(array, left, right);
if(piovt - 1 > left) {
stack.push(left);
stack.push(piovt - 1);
}
if(piovt + 1 < right) {
stack.push(piovt + 1);
stack.push(right);
}
}
}
七、归并排序
时间复杂度:O(N*logN) 归并排序在合并过程中每层都要遍历N次,一共logN层
空间复杂度:O(N) 归并排序在最后合并的时候要额外申请与原来数组一模一样大小的数组
归并排序的缺点是空间复杂度过大
稳定性:稳定
原始分开的区间从0下标开始和原始分开的区间不从0下标开始的合并过程:
7.1递归实现归并排序
public static void mergeSort(int[] array) {
mergeSort(array, 0, array.length - 1);//参数right传的是array.length-1
}
private static void mergeSort(int[] array, int left, int right) {
if(left >= right) return;
int mid = (left + right) / 2;
//分裂左边
mergeSort(array, left, mid);
//分裂右边
mergeSort(array, mid + 1, right);
//合并,在合并的过程中进行了排序
merge(array, left, right, mid);
}
private static void merge(int[] array, int left, int right, int mid) {
int s1 = left;
int s2 = mid + 1;
int[] tmpArr = new int[right - left + 1];//申请一个新的数组,大小为right-left+1
int k = 0;
while(s1 <= mid && s2 <= right) {//满足这个循环条件说明s1~mid和s2~right这两个区间都同时有数据
if(array[s2] < array[s1]) {//若条件改为array[s2]<=array[s1]则排序变为不稳定
tmpArr[k++] = array[s2++];
}else {
tmpArr[k++] = array[s1++];
}
}
while(s1 <= mid) {
tmpArr[k++] = array[s1++];
}
while(s2 <= right) {
tmpArr[k++] = array[s2++];
}
for (int i = 0; i < tmpArr.length; i++) {
array[i + left] = tmpArr[i];//将tmpArr数组拷回原来数组时,赋给array[i+left],因为原来数组的left有可能不是0
}
}
7.2非递归实现归并排序
在合并之前分的组数不断减少,每组元素不断增多,一个一个为一组(gap为1),再到两个两个为一组(gap为2),如此类推
合并之前mid和right有可能会越界,此时要做出调整
public static void mergeSortNor(int[] array) {
int gap = 1;
while(gap < array.length) {
for (int i = 0; i < array.length; i += 2*gap) {
int left = i;
int mid = left + gap - 1;
int right = mid + gap;
if(mid >= array.length) {//mid有可能会越界,越界时要做出调整
mid = array.length - 1;
}
if(right >= array.length) {//right有可能会越界,越界时要做出调整
right = array.length - 1;
}
merge(array, left, right, mid);
}
gap *= 2;
}
}
八、计数排序
时间复杂度:O(2N+数据范围) 即O(MAX(N,数据范围))
空间复杂度:O(数据范围)
稳定性:稳定,但以下代码实现的计数排序是不稳定的
计数排序适合排序某个区间内且集中的数据
public static void countSort(int[] array) {
int minVal = array[0];
int maxVal = array[0];
//求数组中的最大值和最小值
for (int i = 0; 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];//依据最大值和最小值来确定计数数组的大小
//遍历原来的数组进行计数
for (int i = 0; i < array.length; i++) {
count[array[i] - minVal]++;//计数数组的下标相当于要排序数组的元素内容
}
//遍历count,把当前元素写回array
int index = 0;
for (int i = 0; i < count.length; i++) {
while(count[i] != 0) {
array[index] = i + minVal;
index++;
count[i]--;
}
}
}