数据结构之 排序(Java实现)

简单算法:

冒泡排序:

是一种交换排序,基本思想是:俩俩比较相邻记录的关键字,如有反序则交换,直到没有反序的记录为止。

实现(这里从后往前确定最大数进行升序排序):

public void bubble(int[]  array){
        for(int i = 0;i < array.length;i++){
            for(int j = 0;j < array.length - i - 1;j++){
                if(array[j + 1] < array[j]){
                    swap(array,j,j + 1);
    
                }
            }
        }
    }
 public void swap(int[] array,int a,int b){
        int cur = array[a];
        array[a] = array[b];
        array[b] = cur;

    }

优化:

假如我们要对 2,1,3,4,5,6,7进行升序排序,实际上只发生了对1和2进行交换的操作,剩下所有数字都是有序的,但是上述代码还对已经有序的数组进行比较,做了大量无用功。所以我们可以利用这一点对冒泡排序进行优化:设置一个标志位。如下所示:

  public void bubble(int[]  array){
        for(int i = 0;i < array.length;i++){
            boolean flag = true;//每一遍循环前设置为true
            for(int j = 0;j < array.length - i - 1;j++){
                if(array[j + 1] < array[j]){
                    swap(array,j,j + 1);
                    flag = false;//如果发生交换设置为false
                }
            }
//标志位为true说明这一遍循环没有发生交换,即为有序
//标志位为false则继续进行排序
            if(flag){
                break;
            }
        }
    }


 public void swap(int[] array,int a,int b){
        int cur = array[a];
        array[a] = array[b];
        array[b] = cur;

    }

复杂度分析:最好情况:数组本来就有序,则在优化后只在i = 0时进行了n - 1次比较就通过标志位结束排序,此时时间复杂度可达到: O(n)

最坏情况:数组逆序 需要进行1+2+3+......+n - 1次比较,时间复杂度:O(n^2)

因此冒泡排序时间复杂度为O(n^2)

稳定性:稳定排序

空间复杂度:O(1)

简单选择排序

:从n - i + 1个数据中通过比较选出关键字最大或最小的记录并和第i 个记录交换(1<=i<=n). i从1到n逐次变化

 public void select(int[] array){
        for(int i = 0;i < array.length;i++){
            int flag = i;
            for(int j = i + 1;j < array.length;j++){
                if(array[j] < array[flag]){
                    flag = j;
                }
            }
            swap(array,flag,i);
        }

    }

复杂度分析:

时间复杂度:无论情况好坏都需要进行1+2+3+......+n - 1次比较,复杂度为O(n^2)

空间复杂度:O(1)

稳定性:不稳定排序(在与第i个记录交换时可能会使同样大的数的顺序打乱)

直接插入排序:

将一个记录插入到已经有序的记录中得到一个新的,记录数加1的有序表

  public void chaRu(int[] array){
        for(int i = 1;i < array.length;i++){
            int j = i - 1;
            while(j >= 0){
                if(array[j] <= array[j + 1]){
                    break;
                }else{
                    swap(array,j,j + 1);
                }
                j--;
            }
        }
    }

复杂度分析:

时间复杂度:最好情况:数组基本有序:复杂度O(n),最坏情况数组逆序 复杂度为:O(n^2)

则直接插入排序的复杂度为O(n^2)

空间复杂度:O(1)

稳定性:稳定排序

改进算法:

希尔排序

又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录以这个整数为间隔分成多个组, 所有距离为这个整数的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当整数到达1时,所有记录在统一组内排好序。

 public void xiEr(int[] array){
        int count = array.length / 2;
        while(count > 0){
            for(int i = count;i < array.length;i++){
                int j = i - count;
                int flag = array[i];
                while(j >= 0){
                    if(array[j] <= array[j + count]){
                        break;
                    }else{
                        array[j + count] = array[j];
                    }
                     j -= count;
                }
                array[j + count] = flag;
            }
            count /= 2;
        }
    }

复杂度分析:

时间复杂度:取决于增量的选取,并且至今没有一个最好的增量确定方式,但综合下来其时间复杂度一定是突破了O(n^2)的,在上述代码中我们的增量取了数组长度的一半并依次减半,这时候的时间复杂度可以达到O(n^1.25) - O(1.6*n^1.25)

空间复杂度:O(1)

稳定性:不稳定排序

堆排序

将待排序的序列构建成一个大根堆或一个小根堆(升序构建大根堆,降序构建小根堆),则此时堆顶元素就是我们这个序列的最大或最小值,将其与对末尾的元素交换,则此时有序序列的一个元素就确定下来了,再对堆顶元素进行向下调整使其成为一个大根堆或小根堆(注意这里向下调整过程中要去掉上一次我们交换到末尾的元素),重复上述步骤,直到堆只剩下一个元素。

 public void heap(int[] array){
        creadHeap(array);
        int count = array.length - 1;
        while(count > 0){
            swap(array,0,count);
            adjustDown(array , 0,count - 1);
            count--;
        }
    }
    private  void creadHeap(int[] array){
        int parent = (array.length - 1 - 1) / 2;
        while(parent >= 0){
            adjustDown(array,parent,array.length - 1);
            parent--;
        }

    }
    private void adjustDown(int[] array,int parent,int end){
        int son = parent * 2 + 1;
        while(son <= end){
            if(son + 1 <= end && array[son] < array[son + 1]){
                son++;
            }
            if(array[parent] < array[son]){
                swap(array,parent,son);
                parent = son;
                son = parent * 2 + 1;
            }else{
                break;
            }
        }

    }

复杂度分析:

时间复杂度:在外面刚开始构建堆的时候时间复杂度为O(n),之后进行向下调整的时间复杂度为堆的深度O(logi ),i为此时堆的元素的个数,需要进行n - 1次向下调整,

综合一下为:O(n+(logi)*(n-1))

则时间复杂度为:O(nlogn);

空间复杂度:O(1)

稳定性:不稳定

归并排序

将n个记录按照平分的方式划分为2个子序列,再对这俩个子序列按照同样的方式进行划分,直到每个子序列的长度为1,然后俩俩归并得到一个长度为2的子序列,继续进行归并,直到完全合并。即:使每个子序列有序,再使子序列段间有序,将两个有序表合并成一个有序表。

递归实现:

public void mergeSort(int[] array){
        merge(array,0,array.length - 1);
    }
    private void merge(int[] array,int left,int right){
        if(left >= right){
            return;
        }
        int mid = (right + left) / 2;
        merge(array,left,mid);
        merge(array,mid + 1,right);
        combine(array,mid,left,right);
    }
    private void combine(int[] array,int mid,int left,int right){
        int s1 = left;
        int s2 = mid + 1;
        int[] cur = new int[right - left + 1];
        int count = 0;
        while(s1 <= mid && s2 <= right){
            if(array[s2] < array[s1]){
                cur[count++] = array[s2++];
            }else{
                cur[count++] = array[s1++];
            }
        }
        while(s1 <= mid){
            cur[count++] = array[s1++];
        }
        while(s2 <= right){
            cur[count++] = array[s2++];
        }
        for(int i = 0;i < count;i++){
            array[i + left] = cur[i];
        }

    }

非递归实现:

  public void mergeSortNor(int[] array) {
        int gap = 1;
        while(gap < array.length) {
            for (int i = 0;i < array.length;i += 2*gap) {
                int mid = i + gap - 1;
                int left = i;
                int right = mid + gap;
                if(mid >= array.length){
                    mid = array.length - 2;
                }
                if(right >= array.length){
                    right = array.length - 1;
                }
                combine(array,mid,left,right);
            }
            gap = gap * 2;
        }

    }
  private void combine(int[] array,int mid,int left,int right){
        int s1 = left;
        int s2 = mid + 1;
        int[] cur = new int[right - left + 1];
        int count = 0;
        while(s1 <= mid && s2 <= right){
            if(array[s2] < array[s1]){
                cur[count++] = array[s2++];
            }else{
                cur[count++] = array[s1++];
            }
        }
        while(s1 <= mid){
            cur[count++] = array[s1++];
        }
        while(s2 <= right){
            cur[count++] = array[s2++];
        }
        for(int i = 0;i < count;i++){
            array[i + left] = cur[i];
        }

    }

复杂度分析

时间复杂度:每一次二路合并都要将所有记录扫描一遍,复杂度为O(n),整个归并排序要进行log(n)(将归并排序类比成完全二叉树,次数就相当于完全二叉树的深度),则时间复杂度为O(n*logn)

空间复杂度递归实现:每一次进行二路合并都要申请一个子序列2倍大小的存储空间O(n)

同时,如果进行递归需要深度为logn的栈空间 综合则为:O(n+logn)

非递归实现:O(n)

因此:基于空间的角度来说,我们应该尽量考虑使用非递归方法实现归并排序

快速排序

任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

Hoare方法

默认最左边元素left为基准值,设置俩个标记left和right分别从前往后和从后往前进行遍历,只要left遇到大于基准值的元素就停下来,同理,right遇到小于基准值的元素停下来,然后将left和right所对应的元素交换,重复上述过程直到left和right相遇。将基准值与left所对应元素交换。此时基准值左边均为小于等于它的元素,右边均为大于等于它的元素,即左右子序列。在左右子序列中重复上述过程,直到子序列只有一个元素。

 

  public void quickSort(int[] array) {
        quickSortHelper(array,0,array.length - 1);
      
    private void quickSortHelper(int[] array,int left,int right){
        if(left >= right){
            return;
        }
        int div = Sort(array,left,right);
        quickSortHelper(array,left,div - 1);
        quickSortHelper(array,div + 1,right);
    }
    private int Sort(int[] array,int left,int right){
        obtainKey(array,left,right);
        int count = left;
        int flag = array[left];
        while(left < right){

            while(left < right && array[right] >= flag){//这里必须加等号,不加等号就会落入一个交换的循环
                right--;
            }
            //这里必须先从右边减,如果先从左边加,最后left和right相遇的地方的值是比基准大的数
            while(left < right && array[left] <= flag){
                left++;
            }
            swap(array,left,right);
        }
        swap(array,left,count);
        return  left;
    }


挖坑法

默认最左边元素为基准值,将基准值存储在一个变量中,right先从右到左 递减找到一个比基准值小的数覆盖到left位置上,left++直到找到一个比基准值大的数覆盖到right位置上,重复上述操作,直到left和right相遇,此时发现left和right相遇的位置为空,将保存的基准值移到这个位置上,其左右分别为其子序列。

 

 public void quickSort1(int[] array){
        quickSort1Helper(array,0,array.length - 1);
    }
    public void quickSort1Helper(int[] array,int left,int right){
        if(left >= right){
            return;
        }
        int middle = middle(array,left,right);
        quickSort1Helper(array,left,middle - 1);
        quickSort1Helper(array,middle + 1,right);
    }
    public int middle(int[] array,int left,int right){
        int flag = array[left];
        while(left < right){
            while(left < right && array[right] >= flag){
                right--;
            }
            array[left] = array[right];
            while(left < right && array[left] <= flag){
                left++;
            }
            array[right] = array[left];
        }
        array[left] = flag;
        return left;
    }

前后指针法

 

  public void quickSort2(int[] array){
        quickSort2Helper(array,0,array.length - 1);

    }
    public void quickSort2Helper(int[] array,int left,int right){
        if(left >= right){
            return;
        }
        int middle = middle2(array,left,right);
        quickSort2Helper(array,left,middle - 1);
        quickSort2Helper(array,middle + 1,right);

    }
    public int middle2(int[] array,int left,int right){
        int cur = left + 1;
        int temp = left;
        int flag = array[left];
        while(cur <= right){
            if(array[cur] <= flag && array[++temp] != array[cur]){
                swap(array,cur,temp);
            }
            cur++;
        }
        //为什么temp这里交换
        //t因为emp是最后一个小于等于flag的数
        swap(array,temp,left);
        return temp;
    }

快速排序的优化

三数取中法(优化对基准数的选择,使快速排序尽可能成为一个完全二叉树)

从数组前,后,中,三个位置取得三个数,比较得到中间的那个数,交换到最左边成为基准数

 private void obtainKey(int[] array,int left,int right){
        int middle = (left + right) / 2;
        if(array[left] > array[right]){
                if(array[middle] < array[right]){
                    swap(array,left,right);
                } else if (array[middle] > array[left]) {
                    return;
                }else{
                    swap(array,middle,left);
                }
        } else  {
            if(array[middle] > array[right]){
                swap(array,right,left);
            } else if (array[middle] < array[left]) {
                return;
            }else{
                swap(array,middle,left);
            }
        }
尾递归优化

递归是要消耗方法栈的资源的,栈的大小是有限的,如果快排时序列的划分不平衡,且数据量较大,可能会造成栈溢出问题。

并且快排进行到最后是划分序列最多的时候,这时会消耗大量的栈空间。

如果我们能在快排进行到后面时使用其它排序方法,就能节省大量栈空间。

代码如下(这里采用插入排序对尾递归进行优化)

  public void quickSort(int[] array) {
        quickSortHelper2(array,0,array.length - 1);
    }


//到树底时采用插入排序(减少递归,避免栈溢出)
    private  void quickSortHelper2(int[] array,int left,int right){
        if(left >= right){
            return;
        }
        if(right - left <= 3){
            chaRuhelpQuickSort(array,left,right);
            return;
        }
        int div = Sort(array,left,right);
        quickSortHelper2(array,left,div - 1);
        quickSortHelper2(array,div + 1,right);
    }
    private void chaRuhelpQuickSort(int[] array,int left,int right){
        for(int i = left + 1;i <= right;i++){
            int j = i - 1;
            while(j >= left){
                if(array[j] <= array[j + 1]){
                    break;
                }else{
                    swap(array,j,j + 1);
                }
                j--;
            }
        }
    }

快速排序的非递归实现

尽管我们对快速排序进行了优化,但当数据量很大时,快排还是会进行大量递归操作,产生栈溢出问题。这时我们可以利用栈实现快排的非递归方法:

 

 public void quickSortNorRecursion(int[] array){
        Stack<Integer> stack = new Stack<>();
        if(array.length < 2){
            return;
        }
        stack.push(0);
        stack.push(array.length - 1);
        while(!stack.isEmpty()){
            int right = stack.pop();
            int left = stack.pop();
            int mid = quickSortNorRecursionhelper(array,left,right);
            if(mid - 1 > left){
                stack.push(left);
                stack.push(mid - 1);
            }
            if(mid + 1 < right){
                stack.push(mid + 1);
                stack.push(right);
            }
        }
    }
    public int quickSortNorRecursionhelper(int[] array,int left,int right){
        obtainKey(array,left,right);
        int count = left;
        int flag = array[left];
        while(left < right){

            while(left < right && array[right] >= flag){//这里必须加等号,不加等号就会落入一个交换的循环
                right--;
            }
            //这里必须先从右边减,如果先从左边加,最后left和right相遇的地方的值是比基准大的数
            while(left < right && array[left] <= flag){
                left++;
            }
            swap(array,left,right);
        }
        swap(array,left,count);
        return  left;
    }

总结:

 

 

 

从算法的简单性来看,我们将7种算法分为以下两类。 .

简单算法:冒泡、简单选择、直接插入。

改进算法:希尔、堆、归并、快速。

从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。

从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑4种复杂的改进算法。

从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。 从这三组时间复杂度的数据对比中,我们可以得出这样一个认识。堆排序和归并排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情绪化 的天才,心情好时表现极佳,碰到较糟糕的环境会变得差强人意。但是他们如果都来比 赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。

从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有相 应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是O(1)。如果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排序就不是一个较好的决策了。

从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排序是个好算法。

从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适。这也就是我们为什么对快速排序优化时,增加了 一个阈值,低于阈值时换作直接插入排序的原因。(节选自《大话数据结构》)

-

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值