算法和数据结构是一门很重要的课,这里我参照了左神的课程,来记录day1学习笔记
在一个数组中,我要进行一次选择排序,即每一轮遍历找到一个最小值与遍历开头的元素进行替换。这样最终就可以完成从小到大排序。
所以
第一遍遍历(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);
}
}
这里我是用之前的插入排序算法,来进行对数器验证的。
对数器的好处是不利用线上测试平台,自己手动就可以进行测试
/*
* 思路:整型变量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);
}
/*
* 思路:先让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不行