排序算法

1.异或

异或运算的性质与扩展 用来处理奇偶数方便

1)0^N == N N^N == 0 也就是说奇数次异或为最后剩下的那个数,偶数次异或为0 2)异或运算满足交换律和结合率 3)不用额外变量交换两个数

a=a^b b=a^b a=a^b; 这样就完成了两个数的交换

一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到这一个数?

定义一个变量,由于偶数次异或为0,则异或数组中每个变量,剩下的那个就是奇数

一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到这两个数 ?

跟上面一样,先遍历一遍数组,然后那个变量就是temp=a^b。由于是两种数

public class 异或 {
     
    public static void main(String[] args) {
     
        int[] array={
     1,1,1,2,2,2,2,1,2,1,2,3,3,1,1,4,4,4,};
        int num=array[0];
        for (int i = 1; i < array.length; i++)
            num=num ^ array[i] ;
        //遍历完成的到 num=1^ 4

        //由于这两个数不一样 则肯定异或不为0,则num!=0 ,所以num有一位肯定为1
        //提取最右边的1
        int right= (~num +1) &num;
        int num1=0;
        for (int data : array) 
            //然后把数据相当于分成了2组 一个位置有1,一个位置没有1
            if((data & right) == 1)//只要那个位置为1的
               num1^=data;
        System.out.println("两个数为:"+num1+" "+(num1^num));
    }
}

2.二分法(解决局部最小值问题)

使用二分法可以找到局部最小值问题 ,初始判断第1个和第2个数,如果第1个数小,则直接返回第一个数,否则再判断最后两个数,如果最后一个数较小,则直接判断最后一个数。否则中间绝对会有一个最低谷,如下图:

排序算法_第1张图片

3.冒泡排序

思想:每一趟就是 两两比较大小 然后交换数据, 每经历1轮,最大的数就放在最后了,比较的个数少1个

时间复杂度O(N^2),空间复杂度O(1)

		int[] array={
     6,3,8,2,9,1}, temp=0;
        for (int i = 0; i < array.length-1; i++) {
     
            //每一趟就是 两两比较大小 然后交换 每经历1轮,比较的个数少1个,所以结尾减i
            for (int j = 0; j <array.length-1-i ; j++) {
     
                if(array[j] > array[j+1]){
     
                    //抖机灵写法 异或 ^  通过异或来交换两个数的值
                    array[j]= array[j] ^ array[j+1];
                    array[j+1]=array[j] ^ array[j+1];
                    array[j]=array[j] ^ array[j+1];
//                    temp = array[j+1];
//                    array[j+1]=array[j];
//                    array[j]=temp;
                }
            }
    }
}

4.选择排序

思想:用后面的数和每一轮的第一个数相比较,用简单的话来说,就是从第一个数开始,与后面所有的数相比较,找出最小的数,放在第一个位置,以此类推,每一轮确定一个相对于这一轮最小的数。

时间复杂度O(N^2),空间复杂度O(1)

	 //从小到大进行排序
    public  static int[] sort1(int[] array){
     
        int temp=0;
        for (int i = 0; i < array.length-1; i++) {
     
            for (int j = i+1; j <array.length ; j++) {
     
                if(array[i] >array[j]){
     
                    temp=array[i];
                    array[i]=array[j];
                    array[j]=temp;
                }
            }
        }
        return array;
    }
}

5.插入排序

思想:把所有的元素分为两组,已经排序的和未排序的,找到未排序的组中的第一个元素,向已经排序的组中进行插入,插入的时候倒叙遍历已经排序的元素,依次和待插入的元素进行比较,比较的时候进行交换位置,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置。

时间复杂度O(N^2),额外空间复杂度O(1) 。该算法极其不稳定,最坏就是5,4,3,2,1,喊你从小到大排序,最好就是1,2,3,4,5直接遍历一遍O(N)就行,所以及其不稳定。

	int[] array={
     5,4,6,4564,4,6},temp= 0;
        //从小到大进行排序
        for (int i = 1; i < array.length; i++) {
     
            for (int j = i; j >0 ; j--) {
     
                if(array[j-1] >array[j]){
     
                    temp=array[j-1];
                    array[j-1]=array[j];
                    array[j]=temp;
                }
                else
                    break;//因为前面是有序的 如果最新的比最后一个大 那说明可以直接跳出循环了
            }
        }
        System.out.println(Arrays.toString(array));
    }
}

6.归并排序(小和、逆序对问题)

思想:分治合并,把一个数组不断拆分, 左边排好序、右边排好序,然后慢慢合并

时间复杂度O(N*logN),额外空间复杂度O(N)

 public static void main(String[] args) {
     
        int[] array={
     0,9,1,2,5,7,4,-100,6,3,-4,1000};
        //辅助数组
        assist=new int[array.length];
        sort(array,0,array.length-1);
    }

    //进行分治拆分
    public static void sort(int[] array,int low,int high){
     
        if(low >=high)
            return;
        int mid = low+(high-low)/2;
        sort(array,low,mid);//对low到mid之间的元素进行排序;
        //对mid到high之间的元素进行排序;
        sort(array,mid+1,high);//对mid到high之间的元素进行排序;
        //进行归并
        merge(array,low,mid,high);
    }

    private static int[] assist;//归并所需要的辅助数组
    //对数组中,从low到mid为一组,从mid+1到high为一组,对这两组数据进行归并
    private static void merge(int[] array, int low, int mid, int high) {
     
        //low到mid这组数据和mid+1到high这组数据归并到辅助数组assist对应的索引处
        int i=low;//定义一个指针,指向assist数组中开始填充数据的索引
        int p1=low;//定义一个指针,指向第一组数据的第一个元素
        int p2=mid+1;//定义一个指针,指向第二组数据的第一个元素
        //只要有一个数组遍历完了,就要跳出循环
        while (p1<=mid && p2<=high){
     
            if(array[p1] >= array[p2])
                assist[i++] =array[p2++];
            else
                assist[i++] =array[p1++];
        }
        while (p1<=mid)
            assist[i++]=array[p1++];
        while (p2<=high)
            assist[i++]=array[p2++];
        //将值copy放到对应的数组中
        for (int j=low;j<=high;j++)
            array[j]=assist[j];
    }
}

小和问题:在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。 例:[1,3,4,2,5] 1左边比1小的数,没有; 3左边比3小的数,1; 4左 边比4小的数,1、3; 2左边比2小的数,1; 5左边比5小的数,1、3、4、 2; 所以小和为1+1+3+1+1+3+4+2=16 。如果直接尝试暴力循环解法可能会数组越界。则我们可以采取转换思路,相当于把每个位置的信息在归并的时候记录起来了,最后相加。

求左边比他小的数加起来之和可以转换为求当前位置比他大的数的个数之和。

比如:1右边比他大的有4个则如果直接采取暴力的话,遍历完,则肯定有4个1出现。那么我们在归并的时候,就可以记录每次比当前大的数有几个,则该位置数出现几次。归并后,1比他的有4个,则4个1。3右边比他的有2个,则2个3,同理。最后4 * 1+2 * 3+ 1 * 4+ 1 * 2 +5*0=16

private static int[] help;//归并所需要的辅助数组
    public static void main(String[] args) {
     
        int[] array={
     1,3,4,2,5};
        //辅助数组
        help=new int[array.length];
        int res= getMinSum(array,0, array.length-1);
        System.out.println(res);
    }

    private static int getMinSum(int[] array, int left, int right) {
     
        if(left==right) return 0;
        int mid = left + (right-left)/2;
        int sumLeft=getMinSum(array,left,mid);
        int sumRight=getMinSum(array,mid+1,right);
        //进行归并
        return sumLeft+sumRight+merge(array,left,mid,right);
    }

    private static int merge(int[] array, int left, int mid, int right) {
     
        int result=0;
        int i=left,j=mid+1;
        int index=left;//定义一个指针,指向help数组中开始填充数据的索引
        //在归并的时候 如果2个数相等,一定要先放右边数组的
        while (i<=mid && j<=right){
     
            if(array[i] < array[j]){
     
                //如果左边的比右边的小,则右边所有的数一定比这个大,则当前和为右边大的数量*左边的那个数
                result+=(right-j+1) * array[i];
                help[index++]=array[i++];
            }else
                help[index++]=array[j++];
        }
        while (i<=mid) help[index++]=array[i++];
        while (j<=right) help[index++]=array[j++];
        //再放到数组对应的位置上
        if (right + 1 - left >= 0)
            System.arraycopy(help, left, array, left, right + 1 - left);
        return result;
    }

逆序对问题:在一个数组中,左边的数如果比右边的数大,则这两个数构成一个逆序对,请输出逆序对数量。

题目位置

7.快速排序(分类问题)

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

时间复杂度最好是O(nlogn),平均也是O(nlogn),最坏情况为O(n²),快速排序的空间复杂度为O(logn)。

	public static void main(String[] args) {
     
        int[] array={
     0,9,1,2,5,7,4,-100,6,3,-4,1000,2};
        sort(array,0,array.length-1);
    }

    /*切分原理:
    把一个数组切分成两个子数组的基本思想:
    1.找一个基准值,用两个指针分别指向数组的头部和尾部;
    2.先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;
    3.再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置;
    4.交换当前左边指针位置和右边指针位置的元素;
    5.重复2,3,4步骤,直到左边指针的值大于右边指针的值停止。*/
    private static void sort(int[] array, int low, int high) {
     
        if(low>=high)
            return;
        //对数组中,从low到high的元素进行切分
        int partition = partition(array, low, high);
        //对左边分组中的元素进行排序
        sort(array,low,partition-1);
        //对右边分组中的元素进行排序
        sort(array,partition+1,high);
    }

    //返回中间变量的位置角标
    private static int partition(int[] array, int low, int high){
     
        int flag=array[low];
        int left=low;
        int right=high+1;
        int temp=0;
        while (true){
     
            //从右到左扫描比flag小的数
            while (array[--right]>=flag)
                if(right==low)//如果扫描到最左边,则扫描完成
                    break;
                //从左到右扫描比flag大的数
            while (array[++left] <=flag)
                if(high==left)//如果扫描到最右边,则扫描完成
                    break;
            if (left>=right)
                //扫描完了所有元素,结束循环
                break;
            else{
     
                //交换left和right索引处的元素
                temp=array[right];
                array[right]=array[left];
                array[left]=temp;
            }
        }
        //交换此时right和flag的位置
        temp=array[right];
        array[right]=flag;
        array[low]=temp;
        //返回此时right的索引
        return right;
    }

颜色分类问题:虽然快排可以解决,但不是最优解,我们可以遍历一次数组就行。

思想:分区间。设置两个变量left,right,相当于通过left和right把数组分为三部分,left指针之前的部分都小于1,right之后的部分都大于1。中间就是等于1的部分。初始都在边界,代表他俩所包裹的范围是空。从左边进行遍历,当这个数大于了1就和right下标的数交换,此时right–,也就是说right的区间变大了。

排序算法_第2张图片

 public void sortColors(int[] nums) {
     
       if(nums == null) return;
       int left=0,right=nums.length-1,index=0;
       while(index <= right){
     
           if(nums[index] > 1)  swap(nums,index,right--);
           else if(nums[index]==1) index++;
           else swap(nums,left++,index++);
       }
    }

    public static void swap(int[] arr,int i,int j){
     
        int temp=arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }

8.堆排序(top K问题)

堆的特性: 时间复杂度为O(N*logN) 。 空间复杂度为O(1)。

1.它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。

2.它通常用数组来实现。**如果一个结点的位置为k,则它的父结点的位置为[k/2],而它的两个子结点的位置则分别为2k和2k+1。**这样,在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动:从a[k]向上一层,就令k等于k/2,向下一层就令k等于2k或2k+1。

3.每个结点都大于等于它的两个子结点。但是两个子结点的顺序并没有做规定

//这里我们创建大根堆
public class Heap {
     
	 //存储堆中的元素
    private Integer[] items;
    //记录堆中元素的个数
    private int N;
    public Heap(int capacity) {
     
        items =  new Integer[capacity+1];
        N=0;
    }

    public static void main(String[] args) {
     
        Heap heap = new Heap(20);
        for (int i = 0; i < 10; i++) 
            heap.insert(i+1);
        System.out.println(Arrays.toString(heap.items));
        Integer v;
        while ((v=heap.delMax())!=null){
     
            System.out.print(v+" ");
        }
    }

    //交换堆中i索引和j索引处的值
    private void exchange(int i,int j){
     
        int tmp = items[i];
        items[i] = items[j];
        items[j] = tmp;
    }

    //往堆中插入一个元素
    public void insert(int value){
     
        items[++N] =value;
        //插入之后,重新对堆进行排序,不断上浮
        swim(N);
    }

    /*使用上浮算法,使索引k处的元素能在堆中处于一个正确的位置,所以,如果往堆中新插入元素,我们只需要不断的比较新结点a[k]和它的父结点a[k/2]的大小,然后根据结果完成数据元素的交换,就可以完成堆的有序调整。
    */
    private void swim(int k){
     
        //如果已经到了根结点,就不需要循环了
        while (k>1){
     
            if(items[k/2]<items[k])//如果新插入的值比父节点大,则交换位置
                exchange(k/2,k);
            k/=2;
        }
    }

    //删除堆中最大的元素,并返回这个最大元素
    public int delMax(){
     
        int max=items[1];
        exchange(1,N);//先将最大的和最后一个进行交互换
        items[N]=null; //删除最后位置上的元素
        N--;//个数-1
        sink(1); //然后进行下沉,将第一个数据放到合适的位置
        return max;
    }
    
    //使用下沉算法,使索引k处的元素能在堆中处于一个正确的位置
    private void sink(int k){
     
        //如果已经到了最底层,就不需要循环了
        while (k<=N/2){
     
            int maxIndex=0; //记录较大结点的索引
            if(2*k+1<=N){
     //如果满足条件,则表明有右结点
                if(items[2*k]<items[2*k+1])//如果左节点小于右节点
                    maxIndex=2*k+1;
                else maxIndex=2*k;
            }else //如果不存在右结点,较大索引直接是左节点
                maxIndex=2*k;
            if(items[k]>items[maxIndex])
                break;
            else {
     
                //交换两者之间的元素
                exchange(k,maxIndex);
                k=maxIndex;
            }
        }
    }
}

优先队列:PriorityQueue默认是小根堆

要想变成大根堆则:

PriorityQueue<Integer> queue=new PriorityQueue<>((o1,o2) -> o1.compareTo(o2));

9.桶排序

思想:先进先出 按照个、十、百的顺序依次进桶出桶

以下程序在出入桶的过程作了优化 是利用该位的数字出现的频数从个位开始,个位数<= i的个数,再从右往左按照个位数的值,找到count,然后count–就是help数组里对应的下标。

public class RadixSort {
     

    public static void main(String[] args) {
     
        int[] arrs = {
     4,5,123,45,13,435,765,12,1,2,3,345,56,78,9,3};
        if (arrs == null || arrs.length < 2) return;
        int[] result = radixsort(arrs, 0, arrs.length - 1, maxbits(arrs));
    }


    //取出该位置上的数,比如 x =789,d =1 取出个位数字9 d= 2取出十位数字8
    public static int getDigit(int x ,int d){
     
        x = Math.abs(x);
        return ((x / ((int) Math.pow(10, d - 1))) % 10);
    }

    //获得一个数组中最大值的位数 比如数组中的最大值为499,那么就返回3
    public static int maxbits(int[] arrs){
     
        int max = Integer.MIN_VALUE;//获取系统中的最小值,防止越界,可以看做保险措施
        for (int i = 0; i < arrs.length; i++) {
     
            max = Math.max(max, arrs[i]);
        }
        int res = 0;
        while (max != 0){
     
            res++;
            max /=10;
        }
        return res;
    }

    // digit是位数,就看做个、十、百
    public static int[] radixsort(int[] arrs, int begin, int end, int digit) {
     
        final int radix = 10;//写死了,就是10进制
        int i = 0,j = 0;
        int[] help = new int[end - begin + 1];//创建一个帮助数组,大小和原数组一样
        
        //进出桶的次数和该数组的最大位数一样,比如数组中的最大值的位数为499,那么就进出桶3次
        for (int d = 1; d <= digit; d++) {
     
            int[] count = new int[radix];//由于是十进制的排序,那么桶无非就是0.1.2...9
        
            // 遍历数组,统计该位上出现频数 比如针对d=1,则是个位,个位取出来为j,count数组上相应位置+1
            for (i = begin; i <= end ; i++) {
     
                j = getDigit(arrs[i], d);
                count[j]++;
            }
            for (int k = 1; k <radix ; k++) 
                count[k] = count[k]+count[k-1];//累加  达到一种效果  ,比如个位数<=3的个数有7个
                //此时 count[0] 表示 数组中当前位(d位)是0的数字有多少个
                //count[1] 表示 数组中当前位(d位)是0和1的数字有多少个  依次类推 直到count[9]
            
            for (i =end;i>=begin;i--){
     //从右往左
                j = getDigit(arrs[i], d);  //再把相应位上的数取出来,无非就是0-9;
                //出桶,利用help数组。把原始数放到频数-1的位置就是出桶,每放完一次,频数--
                help[count[j] - 1] = arrs[i];
                count[j]--;
            }
            //help数组完成使命,对arrs再做一次规整
            for(i = begin,j = 0;i<=end;i++,j++)  //注意啊,这里的j只是临时变量,用作循环数组help
                arrs[i] = help[j];
        }
        return arrs;
    }
}

你可能感兴趣的:(数据结构与算法,数据结构,排序算法)