目录
1、冒泡排序
2、选择排序
3、位运算交换两数
4、关于位运算
4.1 补码
4.2 按位与(&)
4.3 按位或(|)
4.4 按位异或(^)
4.5 左移(<<)
4.6 右移(>>)
4.7 取反(~)
*4.8 关于位运算的例题
4.8.1 136. 只出现一次的数字
4.8.2 260. 只出现一次的数字 III
5、插入排序
冒泡排序是比较基础的排序算法之一,其思想是相邻的元素两两比较,较大的数下沉,较小的数冒起来,这样一趟比较下来,最大(小)值就会排列在一端。整个过程如同气泡冒起,因此被称作冒泡排序。
冒泡排序的步骤是比较固定的:
1>比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2>每趟从第一对相邻元素开始,对每一对相邻元素作同样的工作,直到最后一对。 3>针对所有的元素重复以上的步骤,除了已排序过的元素(每趟排序后的最后一个元素),直到没有任何一对数字需要比较。
public static void bubbleSort(int[] arr) {
if(arr==null||arr.length<2)
return;
for(int e = arr.length-1; e > 0; e--){//控制比较的轮数,一共n-1轮
for(int i = 0; i < e; i++){//3
if(arr[i]>arr[i+1])
swap(arr,i,i+1);
}
}
}
冒泡排序优化思想:双向遍历
上面的两种优化都是单向遍历比较的,然而在很多时候,遍历的过程可以从两端进行,从而提升效率。因此在冒泡排序中,其实也可以进行双向循环,正向循环把最大元素移动到数组末尾,逆向循环把最小元素移动到数组首部。该种排序方式也叫双向冒泡排序,也叫鸡尾酒排序。
static void bubbleSort(int[] array) {
int arrayLength = array.length;
int preIndex = 0;
int backIndex = arrayLength - 1;
while(preIndex < backIndex) {
preSort(array, arrayLength, preIndex);
preIndex++;
if (preIndex >= backIndex) {
break;
}
backSort(array, backIndex);
backIndex--;
}
}
// 从前向后排序
static void preSort(int[] array, int length, int preIndex) {
for (int i = preIndex + 1; i < length; i++) {
if (array[preIndex] > array[i]) {
swap(array, preIndex, i);
}
}
}
// 从后向前排序
static void backSort(int[] array, int backIndex) {
for (int i = backIndex - 1; i >= 0; i--) {
if (array[i] > array[backIndex]) {
swap(array, i, backIndex);
}
}
}
static void swap (int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,继续放在起始位置知道未排序元素个数为0。
选择排序的步骤:
1>首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。 2>再从剩余未排序元素中继续寻找最小(大)元素,然后放到未排序序列的起始位置。 3>重复第二步,直到所有元素均排序完毕。
public static void selectionSort(int[] arr) {
if(arr==null||arr.length<2)
return;
for(int i=0;i arr[j] ? j : minIndex;
}
swap(arr, i, minIndex);
}
}
选择排序优化思想:
选择排序的优化思路一般是在一趟遍历中,同时找出最大值与最小值,放到数组两端,这样就能将遍历的趟数减少一半。第一次选择最大值与最小值
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];
}
交换的数可以相同,但不能是同一块内存空间的数,即i=j,如果是相同空间,则会将两数的数据抹为0(底层运算与内存分离,两数被暂存在了底层运算当中)
交换a,b的值
int a = 甲;
int b = 乙;
a = a ^ b; // a=甲^乙,b=乙
b = a ^ b; // a=甲^乙,b=甲^乙^乙=甲
a = a ^ b; // a=甲^乙^甲=0^乙=乙,b=甲
首先要明白位运算是在二进制中的运算方式,所有其他进制的数在进行位运算时都要先转化成二进制数再进行运算。
位运算主要包括按位与(&)、按位或(|)、按位异或(^)、取反( ~ )、左移(<<)、右移(>>)这几种。
其中除了取反( ~ )以外,其他的都是二目运算符,即要求运算符左右两侧均有一个运算量。
补码是为了表示一个负数的二进制形式。 其转化方式是先将负数当成正数转化成二进制的形式再将二进制正数的各个位上取反再加上一。
例如-5 先求出5的二进制数 : 0000 0000 0101 然后将各个位上0变1,1变0 : 1111 1111 1010 最后再加1 : 1111 1111 1011
运算的两个数,转换算为二进制后,进行与(&)运算。 当相应位上的数都是1时,该位取1,否则该为0。
例如5 & -5 5 : 0000 0000 0101 -5 :1111 1111 1011 答案 : 0000 0000 0001
运算的两个数,转换为二进制后,进行或(|)运算。 只要相应位上存在1,那么该位就取1,如果都不为1,就为0。
还是5 | -5 0000 0000 0101 1111 1111 1011 可以看到每一位中其中一个都有1 答案 :1111 1111 1111
运算的两个数,转换成二进制数后,进行异或(^)运算 如果相应位置上的数相同,该位取0,如果不同该位取1。
5 ^ -5 0000 0000 0101 1111 1111 1011 答案: 1111 1111 1110
同时任何数异或0都是其本身,一个数如果异或自己则等于0 1)0^N=N N^N=0 2)交换律、结合律 a^b=b^a a^b^c=a^(b^c)
这样我们可以用异或来交换两个数的值
比如交换a,b的值
int a = 甲; int b = 乙; a = a ^ b; // a=甲^乙,b=乙 b = a ^ b; // a=甲^乙,b=甲^乙^乙=甲 a = a ^ b; // a=甲^乙^甲=0^乙=乙,b=甲
将一个数二进制下的数向左移若干位, 比如 x << y 就是将二进制下的x 向左移 y 位
例 : 5 << 5 5 : 0000 0000 0101 5 << 5 : 0000 1010 0000 在10进制下就等于160
我们可以思考一下,在十进制中,一个数每乘一次10就向左进一位。 那么在二进制中,同10进制一样,二进制中每乘一次2就向左进一位, 那么一个数左移x 就等价于一个数乘 2x。
将一个数在二进制下右移若干位 与左移用法相同
例 5 << 2 5:0000 0000 0101 5 << 2 : 0000 0000 0001 十进制下等于1
这里与左移类似,十进制下每除10整数位就退一位 那么右移就等价于除了几次2 同时右移运算是向下取整的
其实在说补码的是后,取反就已经说了,就是将取反的数在二进制下的每一位取相反的数
题目描述:
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
算法思路:
将数组中的每一个数进行异或运算,出现两次的数异或得0,结果就是剩下的只出现一次的数
代码呈现:
public static void printOddTimesNum1(int[] arr){
int eor = 0;
for (int cur : arr){
eor ^= cur;
}
System.out.println(eor);
}
题目描述:
给定一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。你可以按 任意顺序 返回答案。
算法思路:
先全部异或一次, 得到的结果eor=a^b(两个只出现一次的元素), 考察其的某个非0位(比如最右非0位), 那么只出现一次的两个数中, 在这个位上一个为0, 一个为1, 由此可以将数组中的元素分成两部分,异或遍历其中一部分得到eor'(其中一个数a或者b), eor' ^ eor可以得到另一个数。
public static void printOddTimesNum2(int[] arr){
int eor = 0;
for(int curr :arr){
eor ^= curr;
}
//eor = a^b;
//eor != 0;因为两个不同的出现一次的数
//eor必然有一个位置上是1(或者更多)
int rightOne = eor &(~eor+1);//提取出最右的1;
int onlyone = 0;//eor'
for(int cur : arr){
if((cur & rightOne) == 0){//按最右边位为1的那一位数是否为1将数组分成两组,将a、b分开
onlyone ^=cur;//拿到其中的一组进行异或运算得到a或者b
}
}
System.out.println(onlyone+" "+(eor^onlyone));
}
插入排序也是一种常见的排序算法,插入排序的思想是:将初始数据分为有序部分和无序部分,每一步将一个无序部分的数据插入到前面已经排好序的有序部分中,直到插完所有元素为止。 插入排序的步骤如下:每次从无序部分中取出一个元素,与有序部分中的元素从后向前依次进行比较,并找到合适的位置,将该元素插到有序组当中。
public static void insertionSort(int[] arr){
if (arr == null || arr.length< 2){
return;
//e~e有序的
//0~i想有序
for(int i=1;i‹arr.length;i++){// 0~i做到有序
for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]){
//外层遍历到i位置,此时用i和i-1两位置进行比较,直到i前面的数都有序或者到位置为0
swap(arr, j, j + 1);
}
}
}
插入排序优化思想:折半插入排序
该类优化有二分的思想,是在将待排序的元素与有序部分的元素比较时,不再挨个比较,而是用二分折中的方式进行比较,加快比较效率。