五、冒泡排序
public void bubbleSort(int[] array){
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound ; cur--) {
if(array[cur - 1] > array[cur]){
swap(array,cur - 1,cur);
}
}
}
}
性能分析
1. 时间复杂度
最好:O(N) 数据有序
最坏:O(N^2) 数组逆序
平均:O(N^2)。
2.空间复杂度:O(1)
3.稳定性:不稳定
六、快速排序(重要)
原理
- 从待排序区间选择一个数,作为基准值(pivot);
- Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
- 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。
从前往后,找第一个比基准值大的元素
从后往前,找第一个比基准值小的元素
交换这两个位置的元素。
public void quickSort(int[] array){
//参数的含义表示针对数组中的那段区间进行排序
//由于有基准值,采用前闭后闭区间
quickSortHelper(array,0,array.length - 1);
}
public void quickSortHelper(int[] array,int left,int right){
if(left >= right){
//逼近条件很重要
//闭区间,有一个元素或者没有元素
return;
}
//区间整理的方法:选取基准值,小于基准值的放在左侧,大于基准值的放在右侧
int index = partition(array,left,right);//整理完毕后基准值的下标
quickSortHelper(array,left,index - 1);
quickSortHelper(array,index + 1,right);
}
public int partition(int[] array,int left,int right){
//3w2s
int baseIndex = right;
int baseValue = array[baseIndex];//数组最右侧元素,因为是前闭后闭的
while (left < right){//联想到swap
while (left < right && array[left] <= baseValue){
left++;
}
//走到这步,left指向的元素是从左往右走,第一个大于基准值的数
while (left < right && array[right] >= baseValue){
right--;
}
//走到这步,right指向的位置,就是第一个小于基准值的数
swap(array,left,right);
}
//循环结束,两者重合
swap(array,left,baseIndex);
return left;
}
如何证明这里的left对应的元素是比基准值大的
swap(array,left,baseIndex);
循环结束有两种情况
- 由于right–导致的循环结束,此时就是right没有找到比基准值小的元素,但是和left撞上了,并且left的元素是比基准值大的,此时跳出大循环,left指向的元素比基准值大
- 由于left++导致的循环结束,此时就是left没有找到比基准值大的元素,但是和right撞上了,为什么此时的right还是比基准值大呢,因为上一层循环left和right交换了。
如果要取左侧的元素为基准值,1.从后往前找比基准值小的2.从前往后找比基准值大的
(取右侧的元素为基准值,1.从前往后找比基准值大的2.从后往前找比基准值小的。
public int partition(int[] array,int left,int right){
//3w2s
int baseIndex = left;
int baseValue = array[baseIndex];//数组最右侧元素,因为是前闭后闭的
while (left < right){
while (left < right && array[right] >= baseValue){
right--;
}
//走到这步,right指向的位置,就是第一个小于基准值的数
while (left < right && array[left] <= baseValue){
left++;
}
//走到这步,left指向的元素是从左往右走,第一个大于基准值的数
swap(array,left,right);
}
//循环结束,两者重合
swap(array,right,baseIndex);
return right;
}
性能分析
1. 时间复杂度:最坏O(N^2)(逆序,没法达到递归二分的效果)
最好:O(N*log(N))
平均:O(N*log(N))
2.空间复杂度:由于进行了递归,需要调用栈保存层次的关系,需要占据空间的
最好:O(N)
最坏:O(log(N))
平均:O(log(N))
稳定性:不稳定
快速排序的优化
- 如果当前逆序的话,用上面的快速排序效率低
- 如果基准值选的不好,也会影响性能
- 元素数量多,递归深度大,栈就存不下了
优化手段
1.优化取基准值:三元素取中(第一个元素,最后一个元素,中间位置的元素)
2.如果递归深度达到一定层次,不再递归,对当前的待排序区间使用其他排序算法(堆排序)
3.如果当前待排序区间比较小(left,right),使用插排
非递归实现快速排序
用栈的思想来实现递归的操作
代码如下:
public void quickSortByLoop(int[] array){
//1.先创建一个栈,栈里面存的是待处理区间的下标
Stack<Integer> stack = new Stack<>();
stack.push(array.length - 1);
stack.push(0);//此时根节点入栈
while (!stack.isEmpty()){
//3.取栈顶元素,栈顶元素就是我们要处理的区间
int left = stack.pop();//左侧的区间
int right = stack.pop();
if(left >= right){
//一个元素或空,不符合要求,
continue;
}
int index = partition(array,left,right);
//接下来将左右两个区间入栈
stack.push(index-1);
stack.push(left);
stack.push(right);
stack.push(index + 1);
}
}
用栈来模拟递归的过程
七、归并排序
这和我之前写过的一道题很像,叫做归并两个有序链表变成一个有序链表
public void mergeSort(int[] array){
//辅助方法,有三个参数,是区间(前闭后开)
mergeSortHelper(array,0,array.length);
}
public void mergeSortHelper(int[] array,int left,int right){
//类似于后序
if(left >= right || right - left == 1){
//划分的数组为空或着只有一个元素
return;
}
int mid = (left + right)/2;
//[left,mid)
//[mid,right)
mergeSortHelper(array,left,mid);//递归左区间数组
mergeSortHelper(array,mid,right);//递归右区间数组
merge(array,left,mid,right);//合并
}
public void merge(int[] array,int left,int mid ,int right){
//[left,mid)
//[mid,right)
int length = right - left;//创建数组的长度
int[] output = new int[length];
int outputIndex = 0;
int i = left;//想当于链表的cur1,cur2,记录下标的位置
int j = mid;
while (i < mid && j < right){//类似cur1和cur2 都不为空
if(array[i] <= array[j]){
output[outputIndex++] = array[i++];
}else {
//否则将后面的元素添入
output[outputIndex++] = array[j++];
}
}
while (i < mid ){
output[outputIndex++] = array[i++];
}
while (j < right){
output[outputIndex++] = array[j++];
}
for (int k = 0; k < length; k++) {
array[left + k] = output[k];
}
}
public static void main(String[] args) {
SortDemo sortDemo = new SortDemo();
int[] array ={9,5,2,7,6,5,1,8};
sortDemo.mergeSort(array);
System.out.println(Arrays.toString(array));
}
性能分析
1. 时间复杂度:O(n * log(n)) 数据不敏感
2. 空间复杂度:O(N) 数据不敏感
3. 稳定性:稳定(目前只有插入排序,冒泡排序和这个归并排序是稳定的)
优化总结
在排序过程中重复利用两个数组,减少元素的复制过程
归并排序
1. 数据允许在外存中,外部排序,核心思路就是归并排序
2. 归并排序也是一种高效的给链表进行排序的算法
3. 也是各种标准库中稳定排序算法的主要实现方式
非递归版本(了解即可)
借助下标,对整个数组进行分组。
public void mergeSortByloop(int[] array){
//借助下标及相关规律进行分组
//初始情况下,让每个元素单独作为一组
//[0][1] [2][3] [4][5]
//[0,1] 和 [2,3] 合并 [4,5]和[6,7]合并
for (int gap = 1;gap < array.length;gap *= 2){
for (int i = 0; i < array.length; i += 2*gap) {
int beg = i;
int mid = i + gap;
int end = i + 2*gap;
if(mid > array.length){
mid = array.length;
}
if(end > array.length){
end = array.length;
}
merge(array,beg,mid,end);
}
}
}
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N) | O(N^2) | O(N^2) | O(1) | 不稳定 |
选择排序 | O(N^2) | O(N^2) | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N) | O(N^1.3) | O(N^2) | O(1) | 不稳定 |
堆排序 | O(N*log(N)) | O(N*log(N)) | O(N*log(N)) | O(1) | 不稳定 |
快速排序 | O(N*log(N)) | O(N*log(N)) | O(N^2):逆序 | O(log(N))~O(N) | 不稳定 |
归并排序 | O(N*log(N)) | O(N*log(N)) | O(N*log(N)) | O(N) | 稳定 |