数据结构的学习(1)二分查找,利用二分查找找局部最小值,选择排序,冒泡排序,插入排序,位运算的基础知识
一、二分查找某个元素
(1)查找是否存在某个元素在数组中
思想:
1)先看中间位置的值
2)如果中间位置的值大于目标值说明目标值在整个数组中偏左的位置,改变右边界,即Right = Mid - 1;
3)如果中间位置的值小于目标值说明目标值在整个数组中偏右的位置,改变左边界,即Left = Mid + 1;
public static bool Exits(int[] arr,int target){if (arr == null || arr.Length == 0){return false;}int left= 0;int right = arr.Length - 1;while (left <= right){int mid = left + ((right - left) >> 2);if (arr[mid] < target){left = mid + 1;}else if (arr[mid] > target){right = mid - 1;}else{return true;}}return false;}
(2)利用二分查找大于等于某个数的最左侧位置,或者二分查找小于等于某个数的最右侧位置
思想:
1)每次找到符合范围的值,就记录当前的索引,每次砍掉另外一半
2)就比如,查找大于等于某个数的最左侧位置,就每次在更新右边界的时候,记录此时的中间位置,因为什么呢?因为你每次在更新右边界,说明此刻中间值一定是小于等于目标值的,是不是就是偏左的位置,例如 1 2 2 2 4 5,你在找2的最左侧位置。第一次二分,你找到了2,于是更新右边界为2此时新Mid是1,然后在找,发现继续更新右边界为0了,和Left相等了,然后计算此时的Mid,发现不满足条件分支,于是结束了最后一次循环。你在循环中抓住的最后一个Mid索引就是符合题意的位置。
代码:
public static int NearestLeft(int[] arr,int target){int left = 0;int right = arr.Length - 1;int index = -1;while (left <= right){int mid = left + (right - left) >> 2;if (arr[mid] >= target){index= mid;right = mid - 1;}else{left = mid + 1;}}return index;}
二、利用二分查找寻找局部最小值
什么是局部最小值:如下定义:
(1)如果是在0位置,0位置的数小于1位置的数,那么0位置就是局部最小值
(2)同理,如果最后一个位置的数小于倒数第二个位置的数,那么最后一个位置的数就是局部最小值。
(3)在中间时,一定严格满足同时比左边小和比右边小,才叫局部最小值。
(4)给定的数组元素,一定保证相邻元素互不相等,有不有序无所谓
其实按照上面的描述,我们可以如下实现:
1)首先判断边界条件,看数组是否为空或者仅有一个元素,直接返回false,找不到
2)按照上面的条件,如果数组只有两个元素,同时呢0位置的元素小于1位置,说明此时0位置为局部最小值。尾部位置的元素同理
3)判断完了边界条件之后,我们就可以看中间是怎么进行判断的了。
①如果中间值小于中间值右边的第一个元素同时小于左边的第一个元素,说明找到了,直接返回。
②如果中间值仅单独小于某一边元素,但是却大于另一边元素,此时更新边界,如何更新边界呢。就是,移动比自己大的那一边的边界。例如Mid位置<Mid+1位置,则移动右边界,反之移动左边界。为什么?因为你始终要保证边界值一定比边界值前一个元素要大,所以才能肯定能找到局部最小值。
代码:
public int BsGetLocalSmallest(int[] nums)
{if (nums == null || nums.Length == 0){return -1;}if (nums.Length==2|| nums[0] < nums[1]){return nums[0];}if (nums[nums.Length - 1] < nums[nums.Length - 2]){return nums[nums.Length - 1];}int left = 0;int right = nums.Length - 1;while (left <= right){int mid = left + ((right - left) >> 2);if (nums[mid] < nums[mid - 1] && nums[mid] < nums[mid + 1]){return mid;}else if (nums[mid] < nums[mid + 1]){right = mid - 1;}else{left = mid + 1;}}return -1;
}
三、3个O(N²)的排序算法,选择排序,冒泡排序,插入排序
这几个没什么好说的:
交换函数(你可以用位运算写,不过注意用位运算时,交换的对象的引用地址不能是同一个):
public static void Swap(int[] arr, int i, int j)
{int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;
}
选择排序:
第一层循环中记录当前的索引,第二层循环找出最小值的位置,结束本层循环后,与先前记录的索引,交换位置即可。
/*选择排序 */
public static void SelectSort(int[] arr)
{if (arr == null || arr.Length < 2){return;}for (int i = 0; i < arr.Length - 1; i++){//每次遍历找到最小值的下标//然后将当前位置的值和最小值的位置进行交换 那么此时的位置就是最小值int mindex = i;for (int j = i + 1; j < arr.Length; j++){if (arr[j] < arr[mindex]){mindex = j;}}Swap(arr, i, mindex);}
}
冒泡排序:
就是不断比较相邻的数,升序的话。那就是将比自己大的放后面。同样是两层循环。第一层循环从最后一个元素开始,第二层循环从0位置一直到第一层循环记录的索引位置。每次比较,将最大的数放在最后,然后索引前移
/*冒泡排序*/
public static void Bubble(int[] arr)
{//判空 和 单元素 处理if (arr == null || arr.Length < 2){return;}//从尾部开始寻找for (int end = arr.Length - 1; end > 0; end--){for (int i = 0; i < end; i++){if (arr[i] > arr[i + 1]){Swap(arr, i, i + 1);}}}
}
插入排序:
你可以理解为扑克牌排序,我们都是保证从0~0位置,0~1位置,0~2位置,一直到0~ N-1位置,逐渐有序。那么实现起来就很简单,首先第一个元素不用判断吧,所以第一层的循环直接从1开始,然后到末尾。在第二层循环中,我们需要做些什么呢。当然是从上一次层的索引位置的前一个开始,逐步和前面比较,谁小谁放前,每次循环都会保证 ~ 第一层的索引值位置的数有序的。
代码:
/*插入排序*/
public static void InsertSort(int[] arr)
{if (arr == null || arr.Length < 2){return;}//先保证 0~0有序//再保证 0~1有序//……//0~N-1有序// 直接从1开始 每次交换相邻 谁大谁放后for (int i = 1; i < arr.Length; i++){// 当前数 下一个for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){Swap(arr, j, j + 1);}}
}
四、利用位运算进行查找奇数次的数字
请务必记住异或的性质,不进位加法!!!!
例如 1 0 1 0 1 0 1 1 1 0 1 0 1 ^
1 1 1 1 1 0 1 0 0 1 0 1 0 =
0 1 0 1 0 0 0 1 1 1 1 1 1 这就是异或的性质
还有值得记住的就是,任何数和0异或都是自己,任何数和自己异或都是0。好的你已经学会了异或的基本知识辣,我们来两道练手题目吧!
(1)找出一个数组中出现了奇数次的数,其余数都是偶数次数只出现一次的数:
很简单,全部异或一遍不就好了!
class Solution {
public:int singleNumber(vector<int>& nums) {int ans = 0;for(int i = 0;i<nums.size();i++){ans^=nums[i];}return ans;}
};
(2)你已经会找到一个奇数次的数了,很好,很不错,那么我们接下来要加大难度辣。在一个数组中,有两种数,出现了,奇数次。其余的数都出现的是偶数次。
一个小知识,找出一个数作为二进制中最右边的1的位置:
当然是这个数 N&(~N + 1),这一坨,就是只有最右边那个为1的数保留了下来,其余的都去死啦,都变成0辣。为什么呢?
假如N的二进制为 1 0 1 0 1 1 0 1 1 1 0 1 0 1 0 0 1 0 0 0
那么~N的二进制为 0 1 0 1 0 0 1 0 0 0 1 0 1 0 1 1 0 1 1 1
加1后是啥 0 1 0 1 0 0 1 0 0 0 1 0 1 0 1 1 1 0 0 0
与上后是啥 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0
是不是就只剩下最后一个含有1的数了。
好的,回归主线辣,怎么做这个题目呢:
1)你先将每个数都异或起来,异或出来的结果是不是就是两个奇数次的数,我们不妨设为a^b
2)只要他们不为0,是不是就说明,他们的二进制形式一定有一个上面不为0,而是1,我们把这个1找出来。怎么找,利用刚才上述提到的方法找。找出来了,我们命名为rightOne好不好。
3) 那么我们是不是就可以把整个数组中的元素,分为两部分,一部分是和rightOne这个数1位置相同的数,另一部分是不是就是没有就是0,在该二进制位置上面。对不对。
4)很好,你已经快成功了,你已经将上面的数组分成了两部分,怎么分?当然是通过&运算啦,只要结果不为0,说明啥,说明该位置上的都是1.归为一类,否则归为另一类。此刻呢,你只需要重复刚才我们在一堆偶数次数中找计数次的操作就OK了啊!是不是很简单就实现了!!!这时候你已经找到了一个数
5)那么怎么找到另一个呢,当然是和a^b在异或一次就行了,因为相同的数异或起来会为0,0和任何数异或都是自己,而且异或满足交换律和结合律。你只需要记住,相同的一些数,不管他们的位置如何,只要都是异或,那么最后的结果就是不会发生变化的。
代码如下:
//有两个奇数次的数 抓出来public static void printOddTimesTwo(int[] nums){int eor = 0;for(int i = 0; i < nums.Length; i++){eor ^= nums[i];}int onlyOne = 0;//eor'int rightOne = eor & ((~eor) + 1);for(int i = 0;i<nums.Length; i++){if ((rightOne & (nums[i])) != 0){onlyOne &= nums[i];}}Console.WriteLine(onlyOne);Console.WriteLine(eor ^ onlyOne);}