经典算法题

本文深入讲解了五种经典排序算法:数组元素右移、插入排序、桶排序、快速排序和冒泡排序。详细介绍了每种算法的原理、实现代码、性能特点及适用场景,帮助读者全面理解排序算法。

1.给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数

输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]

mport java.util.Arrays;

/**

  • 给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。
  • @author 汤小萌

*/
public class Demo05 {

/**
 * 每次把数组中的元素移动一个位置,移动k轮
 * @param array
 * @param k
 */
public static void moveArrayElement01(int[] array, int k) {
    int length = array.length;
    // 假设移动的位数是newk,那么无论是向右还是向左移动newk+n*length个位置之后,移动到还是原来的位置
    int newk = k % length;
    int temp = 0;
    for(int i = 0; i < newk; i++) {
        // 这个temp保存的是这个数组的一个元素
        temp = array[length - 1];
        for(int j = length - 2; j >= 0; j--) {
            // 让后一个元素等于前面的元素,相当于后面的元素是被前面的元素覆盖了
            array[j+1] = array[j];
        }
        // 第一个元素在每一个循环结束之后就要把之前的临时变量填充后进去
        // 因为临时变量保存的是每一次的最后一个位置的元素
        array[0] = temp;
    }
}

/**
 * 开辟一个新的数组,把旧的数组直接放在新数组中的正确的位置
 * @param array
 * @param k
 */
public static int[] moveArrayElement02(int[] array, int k) {
    int length = array.length;
    int newk = k % length;
    int[] newArray = new int[length];
    // 重复length次把元素从旧位置移动和到新位置
    for(int i = 0; i < length; i++) {
        // 求出元素新的位置
        // 注意是要取模的
        /**
         * 加上i=6(就是最后一个元素)
         * 移动的位置是3
         * 那么 (6+3)%7 = 2
         * 索引原来数组的索引为6(最后一个元素)
         * 在新数组中的索引为2.
         */
        int newPosition = (i + newk) % length;
        newArray[newPosition] = array[i];
    }
    return newArray;
}



public static void main(String[] args) {
    int[] nums = {1,2,3,4,5,6,7};
    int[] re = moveArrayElement02(nums, 3);
    System.out.println(Arrays.toString(re));
}

}

2.插入排序

1、插入排序的思想及原理
插入排序一般分为直接插入排序和二分插入排序,本文只介绍直接插入排序,两者的区别仅在于插入的方式不一样。

插入排序是在待排序数组里插入数据。一般我们认为插入排序就是往一个已经排好序的数列中插入一个元素,使得插入这个数以后,数组仍然有序。

下面具体介绍下插入排序的思路:

首先需要明确待排序的数组由两部分组成,一部分是已经排好序的部分,另一部分是待排序的部分。
接着我们每次选取待排序部分的第一个元素,分别与前面排好序的元素进行比较。当大于前面元素时,可以将该元素直接进入已排好序的部分; 当小于前面元素时,需要把这个元素拿出来暂存,将前面的元素后移,继续与前面的元素相比,直到比较到数组第一个元素或者出现第一个小于拿出的这个元素,这时停止比较、移动,直接把这个元素放到当前空位上。
一直重复步骤2,直到待排元素已经没有元素可进行插入时,停止操作,当前数列为已排好序的数列。

2、插入排序java代码实现
首先最外层必定有个大循环,用于待排序部分的数列。还需要一个内层循环,分别与前面排好序的部分进行比较和移动,直到找到位置可以进行插入。参照扑克牌摸牌后排序

ublic class InsertSort {
    private int[] array;
    public InsertSort(int[] array){
        this.array = array;
    }
    
    public void insertSort(){
        if(array==null){
            throw new RuntimeException("没有待排数组");
        }
        int length = array.length;
        if(length>0){
            for(int i=1;i<length;i++){
                int temp = array[i]; //记录未排好序的第一个元素为temp
                int j = i;   
                
                for(;j>0&&array[j-1]>temp;j--){  //原理中的步骤2
                    array[j] = array[j-1];   //移位
                }
                array[j] = temp;   //插入
            }
        }
    }
    
    public void print(){  //用于打印排完序后的数组
        for(int i=0;i<array.length;i++){
            System.out.println(array[i]);
        }
    }
}

测试程序

public class SortTest {
    public static void main(String[] args) {
        insertSortTest();
    }

    private static void insertSortTest(){
        int[] array = {3,5,0,7,1,4,6};
        InsertSort is = new InsertSort(array);
        is.insertSort();
        is.print();
    }
}

3、插入排序的特点及性能
插入排序和玩扑克牌摸牌后在手中排序一样的原理,比较容易理解。插入排序在序列近似有序时,效率比较高,因为此时减少了比较和移动的次数。

从原理和代码来看,插入排序的时间复杂度尾O(n^2),外层循环执行n次,内层在最坏的情况下也执行n次,并且除了比较操作还有移动操作。最好的情况是序列近似有序,这时内层循环只需比较及移动较少个元素即可完成。当序列本身有序时,插入排序的时间复杂度为O(n)。因此,在数列越有序,效率越高。

空间复杂度为O(1),是常量级的。因为只用了一个变量暂存每次未排好序的首个元素。

插入排序是稳定的排序算法,因为是在相对排好序的基础上进行比较和移动,所以可以保持相对顺序不变,所以是稳定的排序算法。

4、插入排序的适用场景
插入排序的特点是在近似有序的情况下效率比较高。但因为其时间复杂度为O(n^2),所以通常并不单独适用。在所有的排序算法中,我们优先使用快速排序。快速排序在分区规模达到一定的值时(比如10左右),我们改用插入排序算法排该分区。因为此时的分区内数据往往是近似有序的,所以使用快排并不一定优于插入排序。在很多高级语言在内部对快速排序的实现中,也是在分区达到一定规模改用插入排序来排该分区。

3.桶排序

1、桶排序思想
一个简单例子:
对6个人的英语测试成绩(110分)进行排序。假如分数是[6,5,8,8,10,9],用桶排序的思想就是准备10个桶,编号依次为110,将成绩放入对应的桶中,例如6分放入6号桶,两个8分放入8号桶…然后按照桶的标号顺序逐一输出(有就输出,没有就不输出),这就是桶排序的基本思想。

事实上,这只是一个简易版,试想一下,如果待排序的元素跨度范围比较大,例如1~10000,是不是需要10000个桶?实际上这种情况下,一个桶里并非总放一个元素,很多时候一个桶里放多个元素。其实真正的桶排序和散列表有一样的原理。

实际排序中,通常对每个桶中的元素继续使用其他排序算法进行排序,所以更多时候,桶排序会结合其他排序算法一起使用。

2、桶排序代码
在分析了桶排序的思想后,首先要知道待排序元素的范围,以上述为例,声明一个长度为10的数组作为10个桶,然后将成绩逐一往桶中放时,该桶的值+1,最终输出倒序输出数组下标,数组每个位置的值为几就输出几次,这样就能实现基本的桶排序。

public class BucketSort {
    private int[] buckets;
    private int[] array;
    
    public BucketSort(int range,int[] array){
        this.buckets = new int[range];
        this.array = array;
    }
    
    /*排序*/
    public void sort(){
        if(array!=null && array.length>1){
            for(int i=0;i<array.length;i++){
                buckets[array[i]]++;
            }
        }
    }
    
    /*排序输出*/
    public void sortOut(){
        //倒序输出数据
        for (int i=buckets.length-1; i>=0; i--){
            for(int j=0;j<buckets[i];j++){
                System.out.print(i+"\t");
            }            
        }
    }
}

测试代码:

public class SortTest {
    public static void main(String[] args) {
        testBucketsSort();
    }
    
    private static void testBucketsSort(){
        int[] array = {5,7,3,5,4,8,6,4,1,2};
        BucketSort bs = new BucketSort(10, array);
        bs.sort();
        bs.sortOut();//输出打印排序
    }
}

3、桶排序性能特点
桶排序实际上只需要遍历一遍所有的待排元素,然后依次放入指定的位置。如果加上输出排序的时间,就要遍历所有的桶。因此桶排序的时间复杂度是O(n+m),n是待排元素的个数,m是桶的个数,也就是待排元素的范围。这个算法算是相当快的排序算法了,但是空间复杂度比较大。

当待排元素的大小范围比较大,但待排元素个数比较少时,空间浪费就比较严重,待排元素分布月均匀,空间利用率越高,事实上这种情况很少见。

通过以上性能分析,可以得出桶排序的特点:速度快且简单,但同时空间利用率较低。当待排数据跨度很大时,空间利用率是无法忍受的。

4、桶排序适用场景
根据桶排序的特点,桶排序一般适用于一些特定的环境,比如数据范围较为局限或者有一些特定的要求,比如需要通过哈希映射快速获取某些值,需要统计每个数的数量。但是这一切都以确认数据的范围为前提,如果范围跨度过大,则考虑用其他算法。

4.快速排序

1、快速排序思想及原理
事实上,快速排序是堆冒泡排序的一种改进。

它的基本思想是:通过一趟排序将要排序的数据分割为两部分,第一部分所有数据比第二部分的所有数据小,按照这种思路将两部分数据再次分别进行快速排序,可以使用递归完成,最终使得整个数据序列有序。

具体来讲,在待排数据中找一个基准数据(通常取第一个数),接下来将待排数据中比基准数据小的放在待排数据的左侧,将比待排数据中比基准数据大的放在待排数据右侧。此时,左右两个分区的元素相对有序,接着采用上述思路继续对左右两个分区继续排序,直到各分区只有一个元素位置。这里用到了一个典型的分治思想。下面举例说明:

待排序列依次为47、29、71、99、78、19、24、47。为了区分两个47,将后面的47下面增加一个下划线。

步骤:
1、选取一个基准数,一般选第0个元素47。
2、将比基准数小的移动到左侧,比基准数大的移动到右侧,相等的不移动,此时基准数位置为K。
3、对左右两侧重复步骤1和步骤2,直到左右侧细分到只有一个元素。

快排的难点也就是在第2步,怎么移动各个数据?
<1> 首先从数列的右边开始往左找,设下标为j,也就是j–操作,找到第一个比基准数小的值,让他与基准数交换;
<2> 接着开始从左往右找,设下标为i,也就是i++,找到第一个比基准数大的值,让他与基准数交换位置;
<3> 重复1和2,直到i和j相遇时结束,最后基准值所在位置为k。

2、java快排代码

public class QuickSort {
    private int[] array;
    public QuickSort(int[] array){
        this.array = array;
    }
    
    public void printSort(){
        for (int i = 0; i < array.length; i++) {
            System.out.println(array[i]);
        }
    }
    
    public void sort(){
        quicksort(array,0,array.length -1);
    }
    
    private void quicksort(int[] array,int begin,int end){
        if(begin<end){  //i和j没相遇之前比较各数据与基准值大小
            int base = array[begin];  //取第一个值为基准值
            int i = begin;  //左标记为i
            int j = end;    //右标记为j
            
            //一趟排序,找到比基准值大的在基准值右,比基准值小的在基准值左
            while(i<j){
                //从右往左扫描
                while(i<j && array[j]>base){ //从右往左扫,如果元素比基准值大
                    j--;  //则右边标记--,直到找到第一个比基准值小的,停止扫描
                }
                if(i<j){
                    array[i]=array[j];  //交换右扫描第一个比基准值小的数
                    i++;  //i标记右移一位
                }
                //从左往右扫描
                while(i<j && array[i]<base){//从左往右扫,如果元素比基准值小
                    i++;  //则左标记++,直到找到第一个比基准值大的,停止扫描
                }
                if(i<j){
                    array[j]=array[i];  //交换左扫描第一个比基准值大的数
                    j--;  //j标记左移一位
                }
            }  //此时基准值左右两侧相对有序
            
            array[i] = base;  //此时i为中间位置k
            
            quicksort(array,begin,i-1);  //左侧按照快排思路,递归
            
            quicksort(array,i+1,end);    //右侧按照快排思路,递归
        }
    }    
}

测试代码

public class SortTest {
    public static void main(String[] args) {
        teseQuickSort();
    }

    private static void teseQuickSort(){
        int[] array = {3,5,7,3,8,9,6,1,0};
        QuickSort qs = new QuickSort(array);
        qs.sort();
        qs.printSort();
    }
}

3、快排的特点及性能
快排是在冒泡排序之上改进而来的,冒泡排序每次只能交换相邻的两个元素,而快排则是跳跃式的交换,交换距离很大,总的比较次数和交换次数少了很多,速度也快了很多。

快排的平均时间复杂度为O(nlogn),事实上,大多数情况下,排序速度要快于这个时间复杂度。快排实际上是采用的一种分而治之的思想,把问题分解为一个个的小问题去逐一解决,最终在把结果组合起来。

快排因为递归调用,所以空间复杂度为O(logn)。

快排是一种不稳定的排序算法,在经过排序后,等值的元素的相对位置可能发生改变。

快排基本上被认为是相同数量级中所有排序算法中平均性能最好的。

5.冒泡排序

11.动态规划

1.建立状态转移方程
2.缓存并复用以往结果
3.按顺序从小往大算

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值