前言:
在之前的C语言阶段,就已经讲过一遍排序,包括每种排序的细节、原理、代码。
在Java阶段这些排序的方式原理肯定是不变的,只不过要搞清楚在哪些集合中的方法中用到了排序。
例如:
List<Integer> list = new ArrayList<>(); list.sort(new IntCom());
//顺序表中有排序的方法,该方法就用到了快速排序。
接下来,我们就回顾复习一下所有排序的方法。
插入排序:
直接插入排序:
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到****一个新的有序序列。实际中我们玩扑克牌时,就用了插入排序的思想。
综上:
也就是每往后遍历,需要保证前面的都必须是有序的!
如图所示:
......
代码如下:
java
public void Insertsort(int[] arr) {
for(int i = 1;i<arr.length;i++) {
int min = arr[i];
int j = i-1;
for (; j >=0; j--) {
if(min < arr[j]) {
arr[j+1] = arr[j];
}else {
break;
}
}
arr[j+1] = min;
}
}
希尔排序(缩小增量排序):
希尔排序法又称缩小增量法。希尔排序法的基本思想是:
**先选定一个整数,把待排序文件中所有记录分成多个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。**当到达=1时,所有记录在统一组内排好序。
简而言之就是这会将所有数据进行分组,一共分成gap组(gap是可以自己定,但是最后得让gap = 1),每组有n/gap个,每个元素间隔为gap 。
这样排时间效率会变高,因为我们可以发现,直接插入排序排越有序的数组效率越高,所以这个方法是尽量让这个数组趋于有序,最后再进行一次直接插入排序时间效率会提升!
继续拿上一个数组举例:
代码如下:
java
public void Shellsort(int[] arr,int gap) {
while(gap >1) {
gap = gap/3+1;
for (int i = gap; i < arr.length - arr.length % gap; i++) {
int tmp = arr[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (tmp < arr[j]) {
arr[j + gap] = arr[j];
} else {
break;
}
}
arr[j + gap] = tmp;
}
}
}
- 希尔排序是对直接插入排序的优化。
- 当 gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1 时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为 gap 的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定
选择排序:
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序**:**
在元素集合 array[i]--array[n-1] 中选择关键码最大 ( 小 ) 的数据元素
若它不是这组元素中的最后一个 ( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换在剩余的array[i]--array[n-2] ( array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素。
代码如下:
java
public void directSelectSort(int[] arr) {
//记录最小值下标
int min = 0;
for (int i = 0; i < arr.length; i++) {
min = i;
for (int j = i; j <arr.length ; j++) {
if(arr[j]<arr[min]) {
min = j;
}
}
swap(arr,i,min);
}
}
方式二:
如图所示:
用i从left到right遍历,将比arr[max]大的赋值给max,将比arr[min]小的赋值给min。
直到left和right相遇排序结束!
代码如下:
java
public void directSelectSort1(int[] arr) {
int left = 0;
int right = arr.length-1;
while(left<right) {
int min = left;
int max = right;
for (int i = left; i <=right ; i++) {
if(arr[i] > arr[max]) {
max = i;
}
if(arr[i] < arr[min]) {
min = i;
}
}
swap(arr,left,min);
if(max == left) {
max = min;
}
swap(arr,right,max);
left++;
right--;
}
}
堆排序:
堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。( 至于为什么要这样建堆,大家可以翻阅上一篇文章中浏览 )
java
private void siftDown(int[] arr,int parent,int len) {
int child = parent*2+1;
while(child <len) {
if(child+1<len && arr[child+1] < arr[child]) {
child++;
}
if(arr[child]<arr[parent]) {
swap(arr,child,parent);
parent = child;
child = parent*2+1;
}else {
return ;
}
}
}
public void heapSort(int[] arr) {
//建小堆,降序
for(int i = (arr.length-1-1)/2;i>=0;i--) {
siftDown(arr,i,arr.length);
}
//开始排序
for (int i = 0; i < arr.length; i++) {
swap(arr,0,arr.length-1-i);
siftDown(arr,0,arr.length-1-i);
}
}
private void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度: O(N*logN)
- 空间复杂度: O(1)
- 稳定性:不稳定
交换排序:
冒泡排序:
逐一比较,如果不符合要求就两两交换,直到找到最大的或者最小的放在最后,依次往前放。如图:
代码如下:
java
public void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j <arr.length-1-i ; j++) {
//<为升序,>为降序
if(arr[j+1]<arr[j]) {
swap(arr,j,j+1);
}
}
}
}
当然,改代码还可以进行改进,也就是如果一组数据,有一段是有有序的,如果调整完几趟之后发现已经让数组有序了,就不需要再进行j的遍历了!
改进如下:
java
public void bubbleSort(int[] arr) {
for (int i = 0; i < arr.length-1; i++) {
boolean isOrder = true;
for (int j = 0; j <arr.length-1-i ; j++) {
isOrder = false;
//<为升序,>为降序
if(arr[j+1]>arr[j]) {
swap(arr,j,j+1);
}
}
//如果没有进for循环的if语句,那证明全部有序了
if(isOrder) {
return ;
}
}
}
快速排序:
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
Hoare版本:
如图所示:
java
public void quickSort(int[] arr,int left,int right) {
if(left>=right) {
return ;
}
//一次快速排序
int div = partion(arr,left,right);
//左边排
quickSort(arr,left,div);
//右边排
quickSort(arr,div+1,right);
}
//霍尔法快排
private int partion(int[] arr,int left,int right) {
int key = left;
while(left < right) {
//right找小
while(right>left && arr[right] >= arr[key]) {
right--;
}
//left找大
while(left<right && arr[left]<=arr[key]) {
left++;
}
swap(arr,left,right);
}
swap(arr,left,key);
return left;
}
private void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
为了提升思考逻辑能力,建议自己画一遍图,熟悉整个流程:
我先打个样:
以这组数据为例:
首先,如果这个找大和找小的判断语句中没有加==,会变成死循环!!!!
其次,如果先让left走,先找大会造成什么后果?
此时,没有把小的给换到前面,反而换到了中间,有时也可能会导致死循环,使得栈溢出!!
所以正确的做法就是先让right找小,之后再让left找大,最后进行交换!!
......
最后的结果为上图:
之后再一次回归拿到最终答案!!
挖坑法:
如图所示:
java
private int partion2(int[] arr,int left,int right) {
//坑位
int key = left;
//取出坑位的值
int tmp = arr[left];
while(left<right) {
while(left<right && arr[right] >= tmp) {
right--;
}
arr[key] = arr[right];
key = right;
while(left<right && arr[left] <= tmp) {
left++;
}
arr[key] = arr[left];
key = left;
}
arr[key] = tmp;
return key;
}
前后指针法:
代码如下:
java
private int partion3(int[] arr,int left,int right) {
int key = left;
int prev = left;
int cur = left+1;
while(cur != right+1) {
//cur指针找小
if(arr[cur] < arr[key] && arr[++prev] != arr[cur]) {
swap(arr,prev,cur);
}
cur++;
}
swap(arr,key,prev);
return prev;
}
private void swap(int[] arr,int i,int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快速排序的优化:
我们此时可以比较一下这几种排序的对于不同数组的排序时间,我们用到System.currentTimeMillis()方法,获取当前程序运行时间,之后时间作差就得到执行一个排序所用的时间!!
java
public static void testInsertTime(int[] arr) {
int[] tmp = Arrays.copyOf(arr,arr.length);
long time1 = System.currentTimeMillis();
Sort.selectSort(tmp);
long time2 = System.currentTimeMillis();
System.out.println("直接插入排序时间为:"+(time2-time1));
}
public static void testShellTime(int[] arr) {
int[] tmp = Arrays.copyOf(arr,arr.length);
long time1 = System.currentTimeMillis();
Sort.shellSort(tmp);
long time2 = System.currentTimeMillis();
System.out.println("希尔排序时间为:"+(time2-time1));
}
public static void testHeapTime(int[] arr) {
int[] tmp = Arrays.copyOf(arr,arr.length);
long time1 = System.currentTimeMillis();
Sort.heapSort(tmp);
long time2 = System.currentTimeMillis();
System.out.println("堆排序时间为:"+(time2-time1));
}
public static void testQuickTime(int[] arr) {
int[] tmp = Arrays.copyOf(arr,arr.length);
long time1 = System.currentTimeMillis();
Sort.quickSort(tmp,0,tmp.length-1);
long time2 = System.currentTimeMillis();
System.out.println("快速排序时间为:"+(time2-time1));
}
public static void inorderArray(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = i;
}
}
public static void notInorderArray(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = array.length-i;
}
}
public static void initArray(int[] array) {
Random random = new Random();
for (int i = 0; i < array.length; i++) {
array[i] = random.nextInt(100000000);
}
}
public static void main(String[] args) {
int[] array = new int[10000];
inorderArray(array);
testHeapTime(array);
testInsertTime(array);
testQuickTime(array);
testShellTime(array);
}
可以更改数据的数量,还有这些数据的是否为正序,倒序,还是随机都可以测试!!
最终我用正序有序数组、逆序有序数组、随机数组,每组数据是20000个测试结果分别为:
正序有序数组:
逆序有序数组:
随机数组测试结果分别为:
结果显而易见,但是此时的快速排序速率还是不够快,因为要考虑到递归,那就要考虑到栈的溢出,也要考虑到树的高度!!
所以为了不要让快排递归的太深,尽量不要溢出栈,这里采用以下的优化方案:
三数取中法:
因为对于快排,如果快排去排一些有序的数据,它的效率是比较低的,因此这里考虑尽量将这些数据均匀分开:
java
private static int middleNum(int[] arr, int left, int right) {
int mid = left + ((right - left) >> 1);
//mid = (left+right)/2;
if (arr[left] > arr[right]) {
if (arr[left] < arr[mid]) {
return left;
} else if (arr[right] > arr[mid]) {
return right;
} else {
return mid;
}
} else {
if (arr[left] < arr[mid]) {
return right;
} else if (arr[right] > arr[mid]) {
return left;
} else {
return mid;
}
}
}
此时,就可以进行一个小小的优化了。
快速排序非递归:
刚刚体验了递归版的快排的快乐,这会可以试着写一写非递归的快排!
当然我们要借助栈去实现,也就是需要在栈中放入我需要排序的left和right下标!
如图所示:
java
public static void quickSort2(int[] arr) {
Stack<Integer> s = new Stack<>();
s.push(0);
s.push(arr.length-1);
while(!s.isEmpty()) {
int right = s.pop();
int left = s.pop();
if(left >= right) {
continue;
}
int div = partion1(arr,left,right);
s.push(left);
s.push(div);
s.push(div+1);
s.push(right);
}
}
快速排序时间复杂度:
快速排序就相当于是二叉树,但是区分好坏情况,最坏情况时间复杂度为O(N*N),最坏情况空间复杂度为O(N),最好情况时间复杂度为O(N*logN),最好情况空间复杂度为O(logN)。
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫 快速 排序
- 时间复杂度: O(N*logN)
- 空间复杂度: O(logN)
- 稳定性:不稳定
归并排序:
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
递归实现:
java
public static int merge(int[] arr,int left,int right,int[] tmp) {
if(left >=right) {
return left;
}
int mid = (left+right)/2;
int left1 = merge(arr,left,mid,tmp);
int left2 = merge(arr,mid+1,right,tmp);
int right1 = mid;
int right2 = right;
int k = 0;
while(left1<=right1 && left2<=right2) {
if(arr[left1] <arr[left2]) {
tmp[k++] = arr[left1++];
}else{
tmp[k++] = arr[left2++];
}
}
while(left1<=right1) {
tmp[k++] = arr[left1++];
}
while(left2<=right2) {
tmp[k++] = arr[left2++];
}
for (int i = 0; i < k; i++) {
arr[i+left] = tmp[i];
}
return left;
}
非递归实现:
有两种情况:
假设有一组数为个数为偶数:
假设有一组数为个数为奇数:
针对以上的情况写出代码,代码如下:
java
public static void mergeNor(int[] arr) {
//排序区间从小到大
int gap = 1;//每组元素个数为1
while (gap < arr.length) {
for (int i = 0; i < arr.length; i+= 2*gap) {
int left1 = i;
int right1 = left1 + gap - 1;
if(right1 >= arr.length-1) {//任意左或右指针到边界则退出
break;
}
int left2 = left1 + gap;
int right2 = left1 + 2 * gap - 1;
if(right2 >= arr.length-1) {
right2 = arr.length-1;
}
merge(arr,left1,right1,left2,right2);
}
gap = gap*2;
}
}
public static void merge(int[] arr, int left1, int right1, int left2,int right2) {
int[] tmp = new int[right2 - left1 + 1]; //临时数组
int left = left1;
int i = 0;
while (left1 <= right1 && left2 <= right2) {
if (arr[left1] > arr[left2]) {
tmp[i++] = arr[left2++];
} else {
tmp[i++] = arr[left1++];
}
}
while (left1 <= right1) { //如果左指针先循环完,剩下的数补到tmp上
tmp[i++] = arr[left1++];
}
while (left2 <= right2) { //如果右指针先循环完,剩下的数补到tmp上
tmp[i++] = arr[left2++];
}
for (int j = 0; j < i; j++) { //tmp还原到原数组
arr[left + j] = tmp[j];
}
}