数据结构与算法之排序

数据结构与算法之排序

排序算法的稳定性

定义:

数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个算法是稳定的。

稳定性的意义:比如现在有一群学生,我们给他们评奖学金。我们先按照出勤率来排序,然后我们再按照学习成绩来排序。因为学习成绩比出勤率的重要性高,之前出勤率低的人如果学习成绩好,他也能排到前面去。但是关键在于当有不少人的学习成绩是一样的时候,他们的排序的先后顺序就要按照出勤率来排。你们的成绩一样,谁出勤率越高说明对待学习越认真,当然排的更高了。这时候就体现出来算法稳定性的作用了,如果我们用一个不稳定的算法来排序,学习成绩一样的人并没有保持之前的出勤率先后顺序排布,出勤率低的人反而排到了出勤率高的人前面去了,这样就会造成相同成绩的但是出勤率更高的学生的不满。如果按照这个标准评定奖学金,就是很不公平也不符合常理的。

插入排序

步骤:
1.把所有的元素分为两组,已经排序的和未排序的;
2.找到未排序的组中的第一个元素,向已经排序的组中进行插入;
3.倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位;

我的理解

在代码实现方面,插入排序是遍历数组中每一个元素,其之前的子数组都是已经从小到大排序好的数组。对每一个元素进行插入排序都是在进行一个子循环。
举个例子:
2 3 5 6 7 | 4 8 0
现在,当前数组的元素4之前,是已经从小到大排序好的子列。如何进行元素4的插入排序?
在这里插入图片描述
从图上看是一个循环的过程,一旦当前元素比前一个元素要小,则交换。如果当前元素比前一个元素要大或等于前一个元素,因为它之前的子列都是从小到大排序好的,所以它就保持原位,而且循环也无需再进行下去。

public static LinkedList<Integer> insert(LinkedList <Integer> array)
    {
        int n = array.size();
        for(int i=1;i<n;i++)//第一层大循环遍历元素
        {
            for(int j=i;j>=1;j--)//第二层子循环进行插入排序
            {
                int s = array.get(j).intValue();
                int t = array.get(j-1).intValue();
                if(s<t)//这里很重要,如果改成<=那么插入排序就不稳定了
                {
                    array.set(j-1,s);
                    array.set(j,t);
                    System.out.println(array);
                }
                else
                {
                    break;
                }
            }
        }
        return array;
    }

稳定性:稳定

希尔排序

步骤:
1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序;
3.减小增长量,最小减为1,重复第二步操作。

在这里插入图片描述

我的理解

增长量h的初始值通常设为待排序的数组长度的1/2。然后每次减半,直到为1。
在这里插入图片描述
这里分步列出了当h=4时的排序情况。此后,h减半变为2,再减半变为1。当h不为1时的希尔排序本质上也是插入排序,只不过不和前一个元素比较,而是和距离当前位置为h的元素进行比较。h变为1时希尔排序就变为了普通的插入排序。但此时,数组已经基本有序,所以插入排序所做的交换和比较的次数明显变少了。

public static LinkedList<Integer>  Hillsort(LinkedList<Integer> array) {
        int n = array.size();
        for (int i = n/2; i >=1; i/=2)
        {
            for(int j=i;j<n;j++)
            {
                for(int k=j;k>=i;k-=i)
                {
                    int s = array.get(k).intValue();
                    int t = array.get(k-i).intValue();
                    if(s<t)//这里和插入排序一样,改成<=则不稳定
                    {
                        array.set(k,t);
                        array.set(k-i,s);
                        System.out.println(array);
                    }
                    else
                        break;
                }
            }
        }
        return array;
    }

稳定性:不稳定。同组之间相当于是间隔为h的插入排序,是稳定的。但是不同组之间的插入排序可能造成算法的不稳定。

归并排序

在这里插入图片描述
归并排序总得来看是一个递归方法。拿到一个待排序数组,就将它分成两部分,然后将这两部分分别进行排序(这部分就是递归部分),然后再将排序后的两部分子列合并在一起,合并过程中也要进行一次排序。
递归的终止条件就是当待排序数组只有一个元素时,不做操作,直接return。
比较重要的部分就是合并&排序的操作。
现在有两个数组,这两个数组都是从小到大排序好的,如何把他们组合成一个数组并且也按从小到大排序?
如 1,3, 5,7 和 2, 4,6,8, 10,如何将它们组合并排序为:1 2 3 4 5 6 7 8 10?
解决方法是,用两个指针分别指向两个数组的开头。用一个新数组保存结果。每次都比较两个指针所指向的元素的大小,将那个较小的元素插入到新数组中,然后将指向较小元素的指针向后移动一位。当某一个数组的元素全部插入到新数组中以后,就将另一个数组的指针之后的所有元素都插入到新数组中。每次合并&排序都要进行n次插入的操作,所以时间复杂度都是O(n)。
在这里插入图片描述
整个归并排序的过程要合并log2(n)次,每次合并的时间复杂度都是O(n)。所以总时间复杂度为O(n*logn)。
在这里插入图片描述

class sort
{
    public static void Sort(int[] array, int start, int end)
    {
        if(start==end)//如果当前数组只有一个元素,则直接return
            return;
        int n = end-start;
        int h = (n-1)/2;//这里要注意,当数组长度为奇数时,
        //这样设置可以让左子列比右子列少,更符合我个人习惯
        Sort(array,start,start+h);//递归排序左子列
        Sort(array,start+h+1,end);//递归排序右子列
        merge(array,start,h,end);//将排序好的左子列和右子列进行合并&排序

    }
    public static void merge(int[] array,int start, int h,int end)
    {//合并操作,h是
        int l = start;//左子列指针初始值
        int r = start+h+1;//右子列指针初始值
        int i = l;
        int j = r;
        int[] orderd = new int[array.length];//建立新数组用于保存合并结果
        int k = 0;//k表示新数组中下一个需要插入的位置
        while((i-l)<=h&&j<=end)
        //当左子列和右子列都没有完全填入orderd内时,继续循环
        {
            if(array[i]<=array[j])//这里如果改成<就变不稳定了
            {
                orderd[start+k] = array[i];
                k++;
                i++;
            }
            if(array[j]<array[i])//这里如果变>=就变不稳定了
            {
                orderd[start+k] = array[j];
                k++;
                j++;
            }
        }
        if((i-l)<=h)
        {
            while((i-l)<=h)
            {
                orderd[start+k]=array[i];
                i++;
                k++;
            }
        }
        //将左子列或右子列中的剩余元素插入orderd内
        if(j<=end)
        {
            while(j<=end)
            {
                orderd[start+k]=array[j];
                j++;
                k++;
            }
        }
        
        for(i=start;i<=end;i++)
        {//将orderd的元素按对应位置复制到原数组中
            array[i] = orderd[i];
        }
    }

}

稳定性:稳定,归并排序的稳定性主要取决于合并操作。
比如
1 2 4a 7 和 3 4b 8 9
用4a和4b来区分左右子列中的值为4的元素,按照之前的顺序,左子列中的4a应该排在右子列中的4b前面。我们做完一次合并操作以后,数组合并为:1 2 3 4a 4b 7 8 9。4a和4b保持了以前的顺序。归并排序的稳定性主要由代码中注释部分的条件判断决定。如果左子列指针元素小于或等于右子列指针元素的时候,把左子列指针对应的元素插入roderd数组中,这样就保证了左子列中的相等元素先插入数组,保持了稳定性。

快速排序

1.首先设定一个分界值,通过该分界值将数组分成左右两部分。
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
总体还是用递归的方法求解。
步骤为
1.将数组分为左右两部分。将小于array[0]的元素放到左边,大于array[0]的元素放到右边。
2.将左边的部分进行快速排序(递归)
3.将右边的部分进行快速排序(递归)
跳出递归的条件:
输入进来的数组只有一个元素,则不作任何操作,直接return。

class QUICKSORT{
  public int cut(int[] nums, int start, int end)//先写cut函数
    {
        int i = start;
        int j = end;
        while(true)
        {
            while(nums[j]>=nums[start]&&i!=j)//注意判断条件
            {
                j--;
            }
            while(nums[i]<=nums[start]&&i!=j)//注意判断条件
            {
                i++;
            }
            if(i==j)
            {
                int t = nums[start];
                nums[start] = nums[j];
                nums[j]=t;
                break;
            }
            if(i!=j)
            {
                int t = nums[j];
                nums[j] = nums[i];
                nums[i] = t;
            }
        }
        return j;
    }
    public void quicksort(int[] nums, int start, int end)
    {
        if(nums==null||start==end)
            return;
        int c = cut(nums,start,end);
        if(c>start)//判断条件很重要,一定要有
            quicksort(nums,start,c-1);
        if(c<end)//判断条件很重要,一定要有
            quicksort(nums,c+1,end);

    }
}

两个要点:
1.cut函数中的判断条件(nums[j]>=nums[start]&&j!=i)
2.递归函数quicksort中的判断条件:(c>start)(c<end)
时间复杂度:最坏情况就是这个数组是倒序排列的,那快速排序方法就相当于冒泡排序,所以时间复杂度为O(n2).
每次切分(就是代码中的order函数)的时间复杂度为O(n)。
最好的情况就是,每一次切分选择的基准数字刚好将当前序列等分。
那么则切分了log(n)次。乘以切分的O(n),总得时间复杂度为O(n*log(n)).
在这里插入图片描述
这里第二次切分和第三次切分是合起来看的,其实第二次切分应该是两个O(n/2)的操作,第三次切分应该是四个O(n/4)的操作。合起来以后时间复杂度就都是O(n)了。
稳定性:不稳定。快速排序的稳定性取决于切分的操作。
举个例子,当数组为6, 3, 7, 4a, 8, 2, 4b, 9 ,切分后结果为:2, 3, 4b, 4a, 6, 8, 7, 9。此时,4b跑到了4a之前,这显然是不稳定的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值