4、希尔排序
原理:希尔排序是直接插入排序的增强,希尔排序是将整个序列分成若干个组,然后进行分组插入排序,分组的增量gap从大到小,最终必须为1,当增量为1时,也就是插入排序(gap一般是gap = gap/2不断缩小到1或者gap = (gap/3+1),逐步缩小至1)。
举例如下:
代码实现如下:
public class ShellSort {
public void shellSort(int[] arr) {
int gap = arr.length;//增量
int i,j,temp;
while (gap > 0) {
gap = gap/2;//逐步缩小到1
for (i = gap; i < arr.length; i++) {
if (arr[i] < arr[i-gap]){
temp = arr[i];
for(j = i-gap;j >= 0;j -= gap){//将大于temp的元素放到后面
if(arr[j] > temp) {
arr[j + gap] = arr[j];
}else{
break;
}
}
arr[j+gap] = temp;
}
}
}
}
public static void main(String[] args){
ShellSort m = new ShellSort();
int[] arr = new int[]{3,6,2,8,5,7,3,8,5,8,3,7,0};
m.shellSort(arr);
System.out.println(Arrays.toString(arr));
}
}
结果如下:
算法分析:
(1)性能分析
(2)时间复杂度
希尔排序最重要的地方在于,以前在小步长进行排序后,用大步长进行排序仍然有序;否则,算法在迭代过程中,打乱之前的排序,则算法效率就没有那么好了。在希尔排序中,比较是最主要的操作。
(3)稳定性
希尔排序中,相等的元素可能会交换位置,也就是说,相等元素的前后顺序可能会改变,该算法不稳定。
5、快速排序
快速排序是排序算法中非常重要的一种,也是经常会考到的一种,同时它有多种改进算法,需要细细研究。
原理:快速排序是选定一个基准,将大于基准的放在右边,将小于基准的放在左边,依次递归,最终达到整个序列的有序,一般改进的算法主要是在选定基准时进行改进。
步骤:
(1) 选定基准值后,从后往前进行比较,找到第一个小于基准值的进行交换;
(2) 然后,再从前往后找,找到第一个大于基准的进行交换;
(3) 直到从前往后的比较数组下标大于从后往前的比较数组下标,完成一次循环;
(4) 接着按着上述循环分别比较左右两侧的序列,最终有序。
代码实现:
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public int partition(int[] arr,int low,int high){
int base = arr[low];//基准
while(low < high){
while(low < high && arr[high] >= base){//右侧扫描,找到第一个小于base的元素
high--;
}
arr[low] = arr[high];
while(low < high && arr[low] <= base){//左侧扫描,找到第一个大于base的元素
low++;
}
arr[high] = arr[low];
}
arr[low] = base;
return low;
}
public void quickSort(int[] arr,int low,int high){
if(low < high){
int p = partition(arr,low,high);
quickSort(arr,low,p-1);
quickSort(arr,p+1,high);
}
}
快速排序算法的优化:
(1)随机选定基准
当序列几乎有序时,基准每次选定为第一个值时,快排的性能不好,因此,将基准选择改为随机的。(但当整个序列是乱序的,排序性能可能会下降,有些许的运气成分)
代码实现:
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public int partition(int[] arr,int low,int high){
//排序前将base与随机的数进行交换,此时,基准值具有随机性了
swap(arr,low,(int)(Math.random()*(high-low+1)+low));
int base = arr[low];
int j = low;
for(int i = low+1;i <= high;i++){
if(base > arr[i]){
j++;
swap(arr,i,j);
}
}
swap(arr,low,j);
return j;
}
public void quickSort(int[] arr,int low,int high){
if(low < high){
int p = partition(arr,low,high);
quickSort(arr,low,p-1);
quickSort(arr,p+1,high);
}
}
(2)两路快排
快速排序时,将大于基准的排在基准右边,将小于等于基准的排在基准左边。
代码实现:
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public int partition(int[] arr,int low,int high){
swap(arr,low,(int)(Math.random()*(high-low+1)+low));
int base = arr[low];//每次将基准抛出,相当于对[low+1,...,high]的排序
int i = low+1;//i表示对[low+1,...,i)的比base小(等)的部分排序
int j = high;//j表示对(j,...,high]的比base大(等)的部分排序
while(true){
while (i <= high && arr[i] < base){//左侧扫描,找到第一个比base大的元素
i++;
}
while(j >= low && arr[j] > base){//右侧扫描,找到第一个比base小的元素
j--;
}
if(i > j){
break;
}
swap(arr,i++,j--);
}
swap(arr,low,j);
return j;
}
public void quickSort(int[] arr,int low,int high){
if(low < high){
int p = partition(arr,low,high);
quickSort(arr,low,p-1);
quickSort(arr,p+1,high);
}
}
(3)配合插入排序使用
快排是采用分治思想,也就是递归将问题不断缩小规模进而解决,但当数据规模小,整个序列近乎有序时,采用“递归+不稳定”的方式,效率不一定好,此时利用插入排序,效果会更好一些。
代码如下:
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public void insertSort(int[] arr,int m,int n){
int i,j,temp = 0;
for(i = m+1;i <= n;i++){
temp = arr[i];
for(j = i-1;j >= 0 && arr[j] > temp;j--){//将大于基准的后移
arr[j+1] = arr[j];
}
arr[j+1] = temp;
}
}
public int partition(int[] arr,int low,int high){
swap(arr,low,(int)(Math.random()*(high-low+1)+low));
int base = arr[low];
int j = low;
for(int i = low+1;i <= high;i++) {
if (base > arr[i]) {
j++;
swap(arr, i, j);
}
}
swap(arr,low,j);
return j;
}
public void quickSort(int[] arr,int low,int high,int k){
if(low >= high) return;
if(high-low < k){
insertSort(arr,low,high);
return;
}
int p = partition(arr,low,high);
quickSort(arr,low,p-1,k);
quickSort(arr,p+1,high,k);
}
(4)三路快排
当整个序列中有大量的重复数据时,将整个序列分成小于基准,等于基准,大于基准三部分进行排序,也就是所称的三路快排。用指针从前到后扫描,如果cur指向的数小于base,那么交换arr[cur]和arr[i]的值,然后i++,cur++;cur指向的数等于base, 那么cur++;cur指向的数大于base,那么交换arr[cur]和arr[j]的值,然后j–。当cur > j的时候说明三路都已经完成。
代码实现如下:
public static void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public int[] partition(int[] arr,int low,int high){
swap(arr,low,(int)(Math.random()*(high-low+1)+low));
int temp = arr[low];
int i = low;
int j = high;
int cur = i;
while(cur <= j){
if(arr[cur] < temp){
swap(arr,cur++,i++);
}else if(arr[cur] == temp){
cur++;
}else{
swap(arr,cur,j--);
}
}
return new int[]{i-1,j+1};//[i...j]都等于base,子问题就只需要解决i左边和j右边就行了
}
public void quickSort(int[] arr,int low,int high){
if(low < high){
int[] brr = partition(arr,low,high);
quickSort(arr,low,brr[0]);
quickSort(arr,brr[1],high);
}
}
}
三路快排可以避免很多重复元素再次参与递归,对于大量重复的待排序列,效率有所提升。
算法分析:
(1)性能分析
(2)时间复杂度
当序列接近有序时,以第一个元素为基准时,左右两个序列相差悬殊,此时执行效率最差;
当序列随机分布时,以第一个元素为基准进行分割,左右两个序列个数基本相等,此时执行效率最好;
综上,序列越乱序,快排的性能越好,越正序,快排的性能越差。
(3)稳定性
快速排序中,相同元素可能会因为分区而改变前后顺序,所以,快速排序是不稳定的算法。
(4)空间复杂度
快速排序,每次分割需要一个空间存储临时值,但需要进行Nlog2N次分割处理,所以空间复杂度为O(Nlog2N)。