(左神)数据结构与算法----认识时间复杂度和简单的排序算法day1

文章目录

  • 前言
  • 一、时间复杂度是什么?
    • 示例
  • 二、选择排序
  • 三、冒泡排序
  • 四、插入排序
  • 五、对数器
  • 六、例题
    • 1、在一个整型数组中,已知数组中只有一种数出现了奇数次,其他所有数都出现了偶数次,怎么找到出现奇数次的数(时间为复杂度O(N),空间O(1))
    • 2、已知这个数组中有两种数出现了奇数次,其他所有的数都出现了偶数次,怎么找到这两种数
  • 总结


前言

算法和数据结构是一门很重要的课,这里我参照了左神的课程,来记录day1学习笔记


一、时间复杂度是什么?

时间复杂度作为一个算法流程中,常数操作数量的一个指标。常用0(读作big 0)来表示。简单来说就是这个算法流程中,发送了多少常数操作,进而总结出常数操作数量的表达式。

示例

在一个数组中,我要进行一次选择排序,即每一轮遍历找到一个最小值与遍历开头的元素进行替换。这样最终就可以完成从小到大排序。
所以
第一遍遍历(0~N-1):看了n眼,比较n次,进行1次交换
第二遍遍历(1~N-1):看了n-1眼,n-1次比较,进行1次交换
第三遍遍历(2~N-1):看了n-2眼,n-2次比较,进行1次交换
所以看了多少眼=N+N-1+N-2+…
进行了多少次比较=N+N-1+N-2+…
进行多少次交换=N次
以上就是所有的常数操作,写作=aN^2+bN+C(等差数列)
所以时间复杂度为0(n^2),在常数操作数量级的表达式中,不要低阶项,只要最高阶项,且忽略高阶项的系数

二、选择排序

/*
* 选择排序:1、在0~N-1上先选第一个数为最小值,将这个数与后面的所有进行比较
*         2、每一轮遍历找到一个最小数交换到第一个位置
*         3、内循环结束后,外循环+1,从下一个数开始比较
* */
public class SelectionSort {
    //左神
    public static void selectionSort(int[] arr){
        if (arr == null || arr.length < 2){  //排除极端情况
            return;
        }
        for (int i = 0;i<arr.length-1;i++){  //i~N-1
            int minIndex = i;
            for (int j = i+1;j<arr.length;j++){  //i~N-1上找最小值下标
                minIndex = arr[j] < arr[minIndex] ? j : minIndex;
            }
            swap(arr,i,minIndex);
        }
    }
  //交换
    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }
}

选择排序时间复杂度0(n^2),额外空间复杂度是0(1),因为是有限几个变量

三、冒泡排序

/*
* 冒泡排序 
1、基本思想
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,
如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

2、算法描述
冒泡排序算法的运作如下:
比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
      
3、o(n^2)
* */
public class BubbleSort {
    //左神
    public static void bubbleSort(int[] arr){
        if (arr == null || arr.length < 2){
            return;
        }
        for (int e = arr.length-1;e > 0;e--){  //0~e
            for (int i = 0;i<e;i++){
                if (arr[i] > arr[i+1]){
                    swap(arr,i,i+1);
                }
            }
        }
    }
    //交换arr的i和j位置上的值
    public static void swap(int[] arr, int i, int j) {
        //异或运算,相同为0,不同为1(无进位相加)
        //异或预算:0^N=N   N^N=0
        //例子:a=甲,b=乙
        //1、a=a^b;  a=甲^乙 b=乙
        //2、b=a^b;  a=甲^乙 b=甲^乙^乙=甲
        //3、a=a^b;  a=甲^乙^甲=乙  b=甲
        //前提i位置不能等于j位置,不然等于和自己异或,会变为0
        arr[i] = arr[i]^arr[j];
        arr[j] = arr[i]^arr[j];
        arr[i] = arr[i]^arr[j];
    }
}

冒泡排序时间复杂度0(n^2),额外空间复杂度是0(1)
这里的交换并不是写的常规形式,如:

int tmp = array[j];   //交换。大的数往后走
array[j] = array[j+1];
array[j+1] = tmp;

而是采用了

arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];

上面代码有详细介绍异或运算为什么这样写就可以交换两个数了,这种写法是一种机灵的写法,可以不用额外申请空间,两个值互相玩就可以进行交换了。
但是,用异或运算有一个前提:a和b在内存里是两块独立的区域。值可以相同,但a和b所指向的内存不能一样,否则异或会为0


四、插入排序

import java.util.Arrays;

/*
 * 可以理解成扑克牌,每抽一次牌,保证前面的所有数都有序
 * 时间复杂度0(N^2),按最差情况估计,空间复杂度O(1)
 * 插入排序某些情况下比选择排序和冒泡排序要好,因为那两个排序一定是O(n^2),而插入排序不一定
 * */
public class InsertionSort {
    //左神
    public static void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        //0~0有序的
        //0~i想有序
        for (int i = 1; i < arr.length; i++) { //0~i 做到有序
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                //判断前一个数是否比插进来后的数(arr[j+1])大
                //往前遍历,找到比自己小的数插进去(交换)
                swap(arr, j, j + 1);
            }
        }
        /*for (int a : arr) {
            System.out.print(a + " ");
        }*/
    }
    public static void swap(int[] arr, int i, int j) {
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

时间复杂度0(N^2),按最差情况估计,空间复杂度0(1) 插入排序某些情况下比选择排序和冒泡排序要好,因为那两个排序一定是0(N^),而插入排序不一定,因为有可能给的数列是1234567已经排好了的,都不需要交换,时间复杂度为0(N),但是算法估计的时候要用最差情况下时间复杂度,
所以是0(N^2)

五、对数器

概念:
1、有一个你想要测的方法a
2、实现复杂度不好但是容易实现的方法b
3、实现一个随机样本产生器
4、把方法a和方法b跑同样的随机样本,看看得到的结果是否一样。
5、如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或方法b
6、当样本数量很多时对比测试依然正确,可以确定方法a已经正确

代码实例如下:

public static void main(String[] args) {
        int testTime =500000;   //500000次
        int maxSize = 100;   //长度是0-100
        int maxValue = 100;   //值的范围是-100~100
        boolean succeed = true;
        for (int i =0;i<testTime;i++){
            int[] arr1 = gengrateRandomArray(maxSize,maxValue);//生成一个随机数组,长度随机,值也随机
            int[] arr2 = copyArray(arr1);  //copy
            insertionSort(arr1);   //想测的方法去排序arr1
            comparator(arr2);  //对数器去排序arr2
            if (!isEqual(arr1,arr2)){   //是否每个位置的值一样
                //打印arr1
                //打印arr2
                succeed = false;
                break;
            }
        }
        System.out.println(succeed ? "Nice!" : "Fucking fucked!");

        /*int[] arr = gengrateRandomArray(maxSize,maxValue);
        printArray(arr);
        insertionSort(arr);
        printArray(arr);*/
    }
    //生成随机数组
    public static int[] gengrateRandomArray(int maxSize, int maxValue){
        //Math.random() -> [0,1)所有小数,等概率返回一个
        //Math.random() *N->[0,N)所有小数,等概率返回一个
        //(int)(Math.random()*N) ->[0,N-1)所有的整数,等概率返回一个
        int[] arr = new int[(int)((maxSize + 1)*Math.random())]; //长度随机
        for (int i = 0;i<arr.length;i++){
            arr[i] =(int)((maxValue + 1)*Math.random()) -(int)(maxValue*Math.random());//值也随机
        }
        return arr;
    }
    //复制一个新数组
    public static int[] copyArray(int[] arr){
        if (arr==null){
            return null;
        }
        int[] res = new int[arr.length];
        for (int i = 0;i<arr.length;i++){
            res[i] = arr[i];
        }
        return res;
    }
    //判断每个位置的值是否相等
    public static boolean isEqual(int[] arr1,int[] arr2){
        if ((arr1 == null && arr2 !=null) || (arr1 != null && arr2 == null)){
            return false;
        }
        if (arr1 == null && arr2 ==null){
            return true;
        }
        if (arr1.length != arr2.length){
            return false;
        }
        for (int i = 0;i< arr1.length;i++){
            if (arr1[i] != arr2[i]){
                return false;
            }
        }
        return true;
    }
    //打印
    public static void printArray(int[] arr){
        if (arr ==null){
            return;
        }
        for (int i = 0;i<arr.length;i++){
            System.out.print(arr[i] + "");
        }
        System.out.println();
    }
    //排序
    public static void comparator(int[] arr){
        Arrays.sort(arr);
    }
}

这里我是用之前的插入排序算法,来进行对数器验证的。
对数器的好处是不利用线上测试平台,自己手动就可以进行测试

六、例题

1、在一个整型数组中,已知数组中只有一种数出现了奇数次,其他所有数都出现了偶数次,怎么找到出现奇数次的数(时间为复杂度O(N),空间O(1))

/*
     * 思路:整型变量eor与数组中每个数进行异或运算,最后得到的数即为出现奇数次的数
     * 原因: 偶数个异或为0,奇数个异或为本身,0^N=N,N^N=0
     * */
    public void demo1(int[] arr) {
        int eor = 0;
        for (int cur : arr) {
            eor ^= cur;
        }
        System.out.println(eor);
    }

2、已知这个数组中有两种数出现了奇数次,其他所有的数都出现了偶数次,怎么找到这两种数

/*
     * 思路:先让eor^=curNum得到ab两个奇数的aor=a^b,再提取出eor最右侧的1出来
     *      准备一个变量onlyOne(eor')
     *      eor是a^b的结果,一定非0,说明有一位必然为1,然后取出那一位,再用rightone
     *      和数组中a和b相与,结果要么是a,要么是b,因为要么a的rightone
     *      位的数等于1,要么b的等于1,所以最后取出来的结果若为0,则非a即b
     *      最后输出onlyOne和eor^onlyOne=另一个数
     * */
    public void demo2(int[] arr) {
        int eor = 0;
        for (int curNum : arr) {
            eor ^= curNum;
        }
        //eor = a^b;
        //eor !=0
        //eor必然有一个位置上是1
        int rightOne = eor & (~eor + 1); //提取出最右的1,~取反,取反加1为补码。原来的数&补码=最右侧的1

        int onlyOne = 0;
        for (int cur : arr) {
            if ((cur & rightOne) == 0) { //如果rightOne那一位为1,与出来结果是0
                onlyOne ^= cur;  //onlyOne取得a或者b
            }
        }
        System.out.println(onlyOne + " " + (eor ^ onlyOne));
    }

补充:
(一)eor & (~eor + 1) 是提取出了最右的1,为什么?
答:eor的结果是a^b,ab不相等,所以eor不为零
假设eor值用二进制表示是:eor=111010010
取反+1:~eor=000101110
相与结果:
111010010
000101110
——————
000000010
由此可见提取出最后一个1。
(二)为什么要提取最后一个1呢?
答:其实提取哪一个1都可以,因为1代表的是a和b在此位上一定不同(如果相同异或结果应该是0)。这样就可以将rightOne与数组中所有的数包括a,b做与运算,如果运算结果为0或rightOne,那么得到的数要么只能是a要么只能是b(因为a、b在rightOne为1的那一位上不同,相与结果必然不同),从而分离出a和b。
(三)rightOne与所有数相与可以吗?不应该直接与a,b相与吗?
答:因为其他的数是偶数个的,在onlyOne ^=cur的时候同样被互相抵消了,所以不会影响a和b。例如2出现次数为偶数,第一次,onlyOne = 0^2;第二次,onlyOne = 2^2 =0,从而抵消了。

(四)思考:左神在if ((cur & rightOne) == 0),改为了if ((cur & rightOne) == 1),在弹幕里引起了争议,这样是否正确呢?
答:我认为不行,根据(一)得到的rightOne,假设a是100001010如果该位相同,那么相与的结果是
000000010
100001010
——————
000000010
显然,结果是10,而不是1,所以改为0和rightOne是可以分离a和b的,1不行

总结

第一天学习到了以下内容 1、时间复杂度 2、简单的一些排序 3、对数器的使用

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