文章目录
一、插入排序
(一)直接插入排序
public int[] insertSort (int[] arr) {
//数组是引用类型,需要判空
if(arr == null || arr.length == 0) {
return arr;
}
for(int i = 1; i < arr.length; i++) {
int tmp = arr[i];
int j = i - 1;
for(; j >= 0; j--) {
if(arr[j] <= tmp) {
arr[j + 1] = tmp;
break;
}else {
arr[j + 1] = arr[j];
}
}
if(j < 0) {
arr[0] = tmp;
}
}
return arr;
}
(二)希尔排序(以 length / 2 / 2…为增量标准)
public int[] shellSort (int[] arr) {
if(arr == null || arr.length == 0) {
return arr;
}
int length = arr.length;
while(length > 0) {
length /= 2;
insertSort(arr,length);
}
return arr;
}
public int[] insertSort (int[] arr,int k) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
for(int i = k; i < arr.length; i++) {
int tmp = arr[i];
int j = i - k;
for(; j >= 0; j -= k) {
if(arr[j] <= tmp) {
arr[j + k] = tmp;
break;
}else {
arr[j + k] = arr[j];
}
}
if(j < 0) {
arr[j + k] = tmp;
}
}
return arr;
}
二、选择排序
(一)选择排序
public int[] selectSort (int[] arr) {
if(arr == null || arr.length == 0) {
return arr;
}
for(int i = 0; i < arr.length - 1; i++) {
int min = i;
for(int j = i + 1; j < arr.length; j++) {
if(arr[min] > arr[j]) {
min = j;
}
}
if(min != i) {
int tmp = arr[min];
arr[min] = arr[i];
arr[i] = tmp;
}
}
return arr;
}
(二)堆排序
1.方法一:直接利用现成的PriorityQueue数据结构
public int[] heapSort (int[] arr) {
if(arr == null || arr.length == 0) {
return arr;
}
PriorityQueue<Integer> queue = new PriorityQueue<>(arr.length,new Comparator<Integer>() {
public int compare(Integer e1,Integer e2) {
return e2.compareTo(e1);
}
});
for(int i = 0; i < arr.length; i++) {
queue.offer(arr[i]);
}
for(int i = arr.length - 1; i >= 0; i--) {
arr[i] = queue.poll();
}
return arr;
}
2.方法二:自己实现一个建堆和排序的过程
public int[] heapSort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
createHeap(arr,arr.length);
int size = arr.length;
while(size > 1) {
swap(arr, 0, size - 1);
size--;
shiftDown(arr, size, 0);
}
return arr;
}
//升序排序,建大根堆
private void createHeap(int[] arr,int length) {
int size = (length - 2) / 2;
for(int i = size; i >= 0; i--) {
int parent = i;
shiftDown(arr, arr.length, parent);
}
}
//向下调整
private void shiftDown(int[] arr,int length,int parent) {
int child = parent * 2 + 1;
while(child < length) {
if(child + 1 < length && arr[child + 1] > arr[child]) {
child += 1;
}
if(arr[child] > arr[parent]) {
swap(arr,child, parent);
}else {
break;
}
parent = child;
child = parent * 2 + 1;
}
}
//交换
private void swap(int[] arr,int index1,int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
三、交换排序
(一)冒泡排序
public int[] bubbleSort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
for(int i = 0; i < arr.length - 1; i++) {
boolean flg = false;
for(int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
flg = true;
}
}
if(flg == false) {
break;
}
}
return arr;
}
private void swap(int[] arr,int index1,int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
(二)快速排序
由于快排在数组有序或几乎有序的情况下递归过程可能会变成单叉树,因此我们对快排做一些优化
1.三数取中法,比较范围内中头,中,尾三个数的大小,然后将中间值放到数组的开头(在数组有序或几乎有序的情况下极其有效)
2.我们知道,快排的结果就是构成一棵二叉树,而二叉树越往下走,分支节点越多,需要递归的次数也越多,但此时数据量并不大。数据量不大且几乎有序了,采用直接插入法无疑是最好的选择,可以节省一半以上的递归
1.挖坑法(最常用)
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
boolean flg = false;
//在快排前先判断数组是否有序,如果有序,就直接返回,否则空间复杂度可能会达到O(N)
int i = 0;
for(; i < arr.length - 1; i++) {
if(arr[i] > arr[i + 1]) {
flg = true;
break;
}
}
if(flg == false) {
return arr;
}
quickSort(arr, 0, arr.length - 1);
return arr;
}
private void quickSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
setMid(arr, start, end);
int left = start;
int right = end;
int tmp = arr[start];
while(left < right) {
while(left < right && arr[right] >= tmp) {
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= tmp) {
left++;
}
arr[right] = arr[left];
}
arr[left] = tmp;
quickSort(arr,start,left - 1);
quickSort(arr,left + 1, end);
}
// 尽量保证让快排时的首元素是整个数组的中间值
private void setMid(int[] arr,int start, int end) {
int mid = (end - start) / 2 + start;
//一共六种情况
if(arr[start] > arr[end] && arr[start] > arr[mid]) {
if(arr[mid] > arr[end]) {
swap(arr, start, mid);
}else if(arr[mid] < arr[end]){
swap(arr, start, end);
}
return;
}
//以下的if语句中都有两种情况start位置的值就是位于中间,因此不需要交换
if(arr[mid] > arr[end] && arr[mid] > arr[start]) {
if(arr[start] < arr[end]){
swap(arr, start, end);
}
return;
}
if(arr[end] > arr[start] && arr[end] > arr[mid]) {
if(arr[mid] > arr[start]) {
swap(arr, start, mid);
}
return;
}
}
private void swap(int[] arr,int index1,int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
2.hoare法(交换法,第二常用)
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
quickSort(arr, 0, arr.length - 1);
return arr;
}
private void quickSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
int tmp = arr[start];
int left = start;
int right = end;
while(left < right) {
while(left < right && arr[right] >= tmp) {
right--;
}
while(left < right && arr[left] <= tmp) {
left++;
}
swap(arr, left, right);
}
swap(arr, left, start);
quickSort(arr, start, left - 1);
quickSort(arr, left + 1, end);
}
private void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
3.快慢指针法(第三常用)
(1)版本一,代码较多但逻辑最为清晰且不会有多余的交换
实现思路:
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
quickSort(arr, 0, arr.length - 1);
return arr;
}
private void quickSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
int cur = start + 1;
int prev = start;
while(cur <= end) {
while(cur <= end && arr[cur] <= arr[start]) {
prev++;
cur++;
}
while(cur <= end && arr[cur] > arr[start]) {
cur++;
}
while(cur <= end && arr[prev + 1] <= arr[start]) {
prev++;
}
while(cur <= end && arr[cur] <= arr[start] && arr[prev + 1] > arr[start]) {
swap(arr, prev + 1, cur);
prev++;
cur++;
}
}
swap(arr, start, prev);
quickSort(arr, start, prev - 1);
quickSort(arr, prev + 1, end);
}
private void swap(int[] arr, int index1, int index2) {
int tmp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = tmp;
}
(2)版本二,代码精简但会有一些无用的交换
private void quickSort1(int[] arr, int start, int end) {
if(start >= end) {
return;
}
int cur = start + 1;
int prev = start;
while(cur <= end) {
//相比较上面那种写法,这种写法可能会存在多次无用的交换
//即当arr[cur] < arr[start] && arr[++prev] < arr[start]
//并且cur != prev,这种情况下,就会发生无用的交换
if(arr[cur] < arr[start] && arr[++prev] != arr[cur]) {
swap(arr, cur, prev);
}
cur++;
}
swap(arr, start, prev);
quickSort1(arr, start, prev - 1);
quickSort1(arr, prev + 1, end);
}
(3)版本三,极精简版
private void quickSort2(int[] arr, int start, int end) {
if(start >= end) {
return;
}
int prev = start + 1;
//比第二种更极端,只要arr[i] < arr[start],就要交换
//如果数组顺序是降序,那么每个都要自己和自己交换,最后prev == i == end + 1
for(int i = start + 1; i <= end; i++) {
if(arr[i] < arr[start]) {
swap(arr, prev, i);
prev++;
}
}
//要保证与arr[start]交换的位置的值小于等于arr[start],就用prev - 1位置的值
swap(arr, start, prev - 1);
//同理这里的边界也需要修改
quickSort2(arr, start, prev - 2);
quickSort2(arr, prev, end);
}
4.非递归版本(利用栈实现)
private void quickSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
//利用栈实现
Stack<Integer> stack = new Stack<>();
stack.push(end);
stack.push(start);
while(!stack.empty()) {
int left = stack.pop();
int right = stack.pop();
if(left >= right) {
continue;
}
int tmp = arr[left];
int curLeft = left;
int curRight = right;
while(curLeft < curRight) {
while(curLeft < curRight && arr[curRight] >= tmp) {
curRight--;
}
arr[curLeft] = arr[curRight];
while(curLeft < curRight && arr[curLeft] <= tmp) {
curLeft++;
}
arr[curRight] = arr[curLeft];
}
arr[curLeft] = tmp;
stack.push(right);
stack.push(curLeft + 1);
stack.push(curLeft - 1);
stack.push(left);
}
}
四、归并排序
(一)递归实现
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
mergeSort(arr, 0, arr.length - 1);
return arr;
}
private void mergeSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
int mid = (end - start) / 2 + start;
mergeSort(arr, start, mid);
mergeSort(arr, mid + 1, end);
int left1 = start;
int left2 = mid + 1;
int[] tmp = new int[end - start + 1];
int i = 0;
while(left1 <= mid && left2 <= end) {
if(arr[left1] <= arr[left2]) {
tmp[i++] = arr[left1++];
}else {
tmp[i++] = arr[left2++];
}
}
while(left1 <= mid) {
tmp[i++] = arr[left1++];
}
while(left2 <= mid) {
tmp[i++] = arr[left2++];
}
for(int j = 0; j < i; j++) {
arr[start + j] = tmp[j];
}
}
(二)非递归实现
private void mergeSort(int[] arr, int start, int end) {
if(start >= end) {
return;
}
// i == (right - left) / 2
for(int i = 1; i < arr.length; i *= 2) {
for(int j = 0; j < arr.length; j += i * 2) {
int left = j;
int right = left + i * 2;
int mid = left + i;
if(right >= arr.length) {
right = arr.length;
}
if(mid >= arr.length) {
continue;
}
//左闭右开
int[] num = new int[right - left];
int left1 = left;
int left2 = mid;
int k = 0;
while(left1 < mid && left2 < right) {
if(arr[left1] <= arr[left2]) {
num[k++] = arr[left1++];
}else {
num[k++] = arr[left2++];
}
}
while(left1 < mid) {
num[k++] = arr[left1++];
}
while(left2 < right) {
num[k++] = arr[left2++];
}
for(int m = 0; m < k; m++) {
arr[left + m] = num[m];
}
}
}
}
(三)海量数据的排序问题
问题:由于我们内存是有限的,如果排序的数据量比较小,那么我们完全可以在内存中完成。但是,如果需要排序的数据需要的空间比内存大,那么我们就需要使用外部排序
外部排序:排序过程需要在磁盘等外部存储进行的排序
最常用的外部排序:归并排序。
- 将数据平均分成几十份甚至几百几千份
- 此时内存就可以放下一份,然后在内存里我们可以选择多种排序方法
- 进行二路归并,对所有份进行归并并且进行存储,最终结果就是有序的
五、计数排序
思路:
- 计算数组中最大值与最小值的差值
- 创建差值大小的数组
- 当新数组下标 + min得到的下标 == 需要排序的数组中元素的值时,新数组对应下标位置 + 1
- 最后从头遍历,判断新数组的中哪个位置的值不为0,就将该位置的下标 + min值一个个赋值给原数组
因此我们可以得到:
时间复杂度:O(max(范围,n))
空间复杂度:O(范围)
稳定性:稳定(稳定方法需要用到顺序表和栈的数据结构,且不能是直接计数,而是存储)
public int[] MySort (int[] arr) {
// write code here
if(arr == null || arr.length == 0) {
return arr;
}
countSort(arr);
return arr;
}
private void countSort(int[] arr) {
int max = arr[0];
int min = arr[0];
for(int i = 1; i < arr.length; i++) {
if(max < arr[i]) {
max = arr[i];
}
if(min > arr[i]) {
min = arr[i];
}
}
int[] tmp = new int[max - min + 1];
for(int i = 0; i < arr.length; i++) {
tmp[arr[i] - min]++;
}
int j = 0;
for(int i = 0; i < arr.length; i++) {
while(tmp[i] > 0) {
arr[j++] = i + min;
tmp[i]--;
}
}
}
六、排序算法复杂度与稳定性分析
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(N) | O(N ^ 2) | O(N ^ 2) | O(1) | 稳定 |
希尔排序 | O(N) | O(1) | 不稳定 | ||
选择排序 | O(N ^ 2) | O(N ^ 2) | O(N ^ 2) | O(1) | 不稳定 |
堆排序 | O(N * logN) | O(N * logN) | O(N * logN) | O(1) | 不稳定 |
冒泡排序 | O(N)(优化之后的) | O(N ^ 2) | O(N ^ 2) | O(1) | 稳定 |
快速排序 | O(N * logN) | O(N * logN) | O(N ^ 2) | O(logN) ~ O(N) | 不稳定 |
归并排序 | O(N * logN) | O(N * logN) | O(N * logN) | O(N) | 稳定 |
注意:由于希尔排序最优解的问题暂时没解决,因此在这里只是说明除二增量法的时间复杂度大致为O(N ^ 1.3 ~ 1.5)