二分查找
二分查找法主要是解决在“一堆数中找出指定的数”这类问题。
其中一堆数必须具备的特征:
- 存储在数组中
- 有序排列
算法思想:
在一个包含x的数组内,二分查找通过对范围的跟踪来解决问题。开始时,范围就是整个数组。通过将范围中间的元素与x比较并丢弃一半范围,范围就被缩小。这个过程一直持续,直到在x被发现,或者那个能够包含t的范围已成为空。
中间下标值的计算:
如果写成(low+high)/2,low+high可能会溢出,从而导致数组访问出错。改进的方法是将计算方式写成如下形式:low+ ( (high-low) >>1)。
-
二分查找元素x的下标,如无 return -1
int binarySearch(int *a, int left, int right, int x) { if (nullptr == a || left > right || left < 0) return -1; int mid; while (left <= right)//注意是<=,若是<会找不到边界值情况 { mid = left + ((right - left) >> 1); if (x < a[mid]) right = mid - 1; //[left,mid-1] else if (x > a[mid]) left = mid + 1; //[mid+1,right] else return mid; } return -1; }
-
二分查找返回x(可能有重复)第一次出现的下标,如无return -1
int binS_first(int *a, int low, int high, int x) { if (NULL == a || low > high || low < 0) return -1; int mid; while (low < high) { mid = low + ((high - low) >> 1);//计算中点 if (x > a[mid])// <x ,调整起点或者终点 low = mid + 1; else // >=x high = mid; } if (a[low] == x) return low; return -1; }
我们只需找到x重复出现情况下的第一次出现的下标。则我们只需用a[mid]和元素x进行比较,当a[mid]<x时
此时待查元素肯定在待查区间的右半部分 显然此时 不包括 mid 所以有 low = mid+1, 若a[mid]>=x时, 因为我
们查找的是x第一次出现的位置,我们不关心x最后出现的位置,所以此时high下标为mid,直到 low == high 终止
循环,并且比较a[low]是否为x,若是则 找到。
总的思路是:
把有序序列分成2个序列:[first,mid][mid+1,last) 当 a[mid]<x 时 使用 使用序列[mid + 1,last)
当 a[mid]>=x 时 使用序列[first,mid]。
-
二分查找返回x(可能有重复)最后一次出现的下标,如无return -1
int binS_last(int *a, int low, int high, int x) { if (NULL == a || low > high || low < 0) return -1; int mid; while (low + 1 < high)//** { mid = low + ((high - low) >> 1); if (x >= a[mid]) // <=x low = mid; else // >x high = mid - 1; } if (a[high] == x)//先判断high return high; else if (a[low] == x) return low; return -1; }
在 while中我们假定 low+1 < high,否则在只有两个或者一个元素时 我们只需在while循环之外判断即可。
接下来的while 情况和问题2等价。我们现在关心的是 x(可能有重复)最后一次出现的下标,所以现在我们不关心他
第一次出现下标的位置, 当 a[mid]<=x 时 low = mid, 否则 a[mid] >x 此时 high = mid -1。
二分查找返回x(可能有重复)第一次(最后一次)出现的下标找最小的等号放>=x位置(high),找最大的等号放<=x的位置(low)。
其中a[mid]在和待查找元素x比较中带 = 的,在对low 或者high赋值时一定为 mid,其它情况(<或>)则为mid+(-)1. -
二分查找返回刚好小于x的元素下标,如无return -1。
-
二分查找返回刚好大于x的元素下标。
-
返回有序数列某一个元素重复出现的次数。
时间复杂度:O(log n)
#include
**lower_bound():**返回的是被查序列中第一个大于等于查找值的指针;
用法:int t=lower_bound(a+l,a+r,m)-a
解释:在升序排列的a数组内二分查找[l,r)区间内的值为m的元素。返回m在数组中的下标。
特殊情况:
1.如果m在区间中没有出现过,那么返回第一个比m大的数的下标。
2.如果m比所有区间内的数都大,那么返回r。这个时候会越界,小心。
3.如果区间内有多个相同的m,返回第一个m的下标。
时间复杂度:
一次查询O(log n),n为数组长度。
**upper_bound():**返回的是被查序列中第一个大于查找值得指针;
用法:int t=upper_bound(a+l,a+r,m)-a
解释:在升序排列的a数组内二分查找[l,r)区间内的值为m的元素。返回m在数组中的下标+1。
特殊情况:
1.如果m在区间中没有出现过,那么返回第一个比m大的数的下标。
2.如果m比所有区间内的数都大,那么返回r。这个时候会越界,小心。
3.如果区间内有多个相同的m,返回最后一个m的下标+1。
时间复杂度:
一次查询O(log n),n为数组长度。
二分查找法的缺陷
二分查找法的O(log n)让它成为十分高效的算法。不过它的缺陷却也是那么明显的。就在它的限定之上:
必须有序,我们很难保证我们的数组都是有序的。当然可以在构建数组的时候进行排序,可是又落到了第二个瓶颈上:它必须是数组。
数组读取效率是O(1),可是它的插入和删除某个元素的效率却是O(n)。因而导致构建有序数组变成低效的事情。
解决这些缺陷问题更好的方法应该是使用二叉查找树了,最好自然是自平衡二叉查找树了,自能高效的(O(n log n))构建有序元素集合,又能如同二分查找法一样快速(O(log n))的搜寻目标数。
递归栈溢出
解决方法:
用堆变量取代栈变量
使用循环取代递归
使用尾递归取代普通递归:
尾递归必须满足的几个条件:
- 最后一行是对自身的调用
- 最后一行除了对自身的调用外不得出现别的操作
- 除最后一行外其它地方不能出现对自身的调用
这个方法也有一个很大的缺点,那就是需要编译器的支持,目前只有C语言支持尾递归的优化。
位操作
-
右移一位,表示除以2;左移一位,表示乘以2
-
交换两个数
a ^= b; b ^= a; a ^= b;
-
判断奇偶数
只要根据数的最后一位是 0 还是 1 来决定即可,为 0 就是偶数,为 1 就是奇数。
0==(a&1)
-
交换符号
整数取反加1,正好变成其对应的负数(补码表示);负数取反加一,则变为其原码,即正数
c = ~c + 1;
-
求绝对值
整数的绝对值是其本身,负数的绝对值正好可以对其进行取反加一求得,即我们首先判断其符号位(整数右移 31 位得到 0,负数右移 31 位得到 -1,即 0xffffffff),然后根据符号进行相应的操作
int abs(int a) { int i = a >> 31; return i == 0 ? a : (~a + 1); }
-
统计二进制中 1 的个数
a &= (a-1) 每计算一次,原二进制中的数就少一个1。
count = 0 while(a){ a = a & (a - 1); count++; }
单链表
使用快慢指针找链表的中点:
-
首先我们设置两个指针slow和fast,slow指针每次移动一步,fast指针每次移动两步;
-
如果链表中节点个数为偶数时,当快指针无法继续移动时,慢指针刚好指向中点;如果链表中节点个数为奇数时,当快指针走完,慢指针指向中点前一个节点。
凑零钱问题
转载地址:https://www.cnblogs.com/snowInPluto/p/5992846.html
c++代码实现:
int coinChange(std::vector<int>& coins, int amount) {
int Max = amount + 1;
std::vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (size_t i = 1; i <= amount; ++i) {
for (size_t j = 0; j < coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = std::min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
快速排序
选择一个元素作为基准,并围绕选定的主元素对给定数组进行分区。
选择主元素的方式:
- 总是选择第一个元素作为基准
- 总是选择最后一个元素作为基准
- 选择一个随机的元素作为基准
- 取中值作为基准
快速排序的关键过程是partition(),分区的目标是,给定一个数组和数组的元素x作为基准,将x放在排序数组的正确位置,并将所有小于x的元素放在x左边,将所有大于x的元素放在x右边。所有这些都应该在线性时间内完成。
伪代码:
/* low --> 起始索引值, high --> 终止索引值 */
quickSort(arr[], low, high)
{
if (low < high)
{
/* pi 是分区索引值, 分区后,arr[pi] 现在处在正确的位置*/
pi = partition(arr, low, high);
quickSort(arr, low, pi - 1); // Before pi
quickSort(arr, pi + 1, high); // After pi
}
}
分区算法:
从最左边的元素开始,跟踪较小(或等于)元素的下标i,当遍历发现一个较小的元素,用arr[i]来交换当前元素,否则忽略。
伪代码:
这个函数以最后一个元素为基准, 放置基准元素在它正确的位置,比基准值小的值放在基准值左边,大的放在右边。
partition (arr[], low, high)
{
pivot = arr[high]; 取出一位作为基准值
i = (low - 1) // 较基准值小的元素索引值-1
for (j = low; j <= high- 1; j++) //较基准值大的索引值-1
{
//如果当前元素小于或等于基准值
if (arr[j] <= pivot)
{
i++; // 较基准值小的元素索引值+1
swap arr[i] and arr[j]
}
}
swap arr[i + 1] and arr[high]) //将基准值介于小值和大值中间
return (i + 1) 返回基准值的位置
}
分区示例:
rr[] = {10, 80, 30, 90, 40, 50, 70}
Indexes: 0 1 2 3 4 5 6
low = 0, high = 6, pivot = arr[h] = 70
Initialize index of smaller element, i = -1
Traverse elements from j = low to high-1
j = 0 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 0
arr[] = {10, 80, 30, 90, 40, 50, 70} // No change as i and j
// are same
j = 1 : Since arr[j] > pivot, do nothing
// No change in i and arr[]
j = 2 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 1
arr[] = {10, 30, 80, 90, 40, 50, 70} // We swap 80 and 30
j = 3 : Since arr[j] > pivot, do nothing
// No change in i and arr[]
j = 4 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 2
arr[] = {10, 30, 40, 90, 80, 50, 70} // 80 and 40 Swapped
j = 5 : Since arr[j] <= pivot, do i++ and swap arr[i] with arr[j]
i = 3
arr[] = {10, 30, 40, 50, 80, 90, 70} // 90 and 50 Swapped
We come out of loop because j is now equal to high-1.
Finally we place pivot at correct position by swapping
arr[i+1] and arr[high] (or pivot)
arr[] = {10, 30, 40, 50, 70, 90, 80} // 80 and 70 Swapped
Now 70 is at its correct place. All elements smaller than
70 are before it and all elements greater than 70 are after
it.
c++实现:
int partions(std::vector<int>& nums, int l, int r)
{
int pivot = nums[r]; // 选取数组最右边的元素作为基准
int i = l - 1; // 跟踪较基准值小的元素索引i
for (int j = l; j <= r - 1; ++j) { // 跟踪较基准值大的元素索引j
if (nums[j] <= pivot) { // 如果当前元素小于或等于基准值
i++; // 较基准值小的元素索引值+1
std::swap(nums[i], nums[j]);
}
}
std::swap(nums[i + 1], nums[r]); //最后 将基准值归位
return i + 1; //返回基准值在数组中的位置
}
int randomizedPartition(std::vector<int>& nums, int l, int r)
{
//(rand() % (b-a)) + a [a,b)
//(rand() % (b-a+1)) + a [a,b]
//(rand() % (b-a)) + a + 1 (a,b]
int i = std::rand() % (r - 1 + 1) + 1; //产生一个[1,r]闭区间的随机整数
std::swap(nums[r], nums[i]); //随机更换数组最右边的数
return partions(nums, l, r);
}
void quickSort(std::vector<int>& nums, int l, int r)
{
if (l < r)
{
int pos = randomizedPartition(nums, l, r);
quickSort(nums, l, pos - 1);
quickSort(nums, pos + 1, r);
}
}
算法分析:
快速排序所花费的时间一般可以写成以下形式:
T(n) = T(k) + T(n-k-1) + 0(n)
前两项用于两个递归调用,最后一项用于分区过程。k是小于基准值的元素个数
快速排序所花费的时间取决于输入数组和分区策略。
以下是三种情况::
最坏的情况是,分区过程总是选择最大或最小的元素作为主元素。如果我们考虑上面的分区策略,其中最后一个元素总是选择为主元素,那么最坏的情况将发生在数组已经按递增或递减顺序排序时
T(n) = T(0) + T(n-1) + 0(n)
等价于
T(n) = T(n-1) + 0(n)
以上递归式的解为O(n2)。
最好的情况发生在分区过程总是选择中间元素作为基准值时。
T(n) = 2T(n/2) + 0(n)
以上递归式的解为O(nlogn)。
平均情况:
通过考虑将O(n/9)个元素放在一个集合中,O(9n/10)个元素放在另一个集合中,我们可以得到平均情况的概念。下面是这种情况的递归式
T(n) = T(n/9) + T(9n/10) + 0(n)
以上递归式的解为O(nlogn)。
虽然快速排序最坏的情况下的时间复杂度是O(n2),它比许多其他排序算法(如合并排序和堆排序)都要高,但实际上快速排序更快,因为它的内部循环可以在大多数体系结构和大多数实际数据中有效地实现。快速排序可以通过改变pivot的选择以不同的方式实现,因此对于给定类型的数据,最坏的情况很少发生。然而,当数据很大并且存储在外部存储中时,归并排序通常被认为是更好的选择。
根据原地算法的广义定义,它被定义为原地排序算法,因为它只使用额外的空间存储递归函数调用,而不用于操作输入。
二叉树
定义:n(n>=0)个结点的有限集合,该集合或者为空集,或者由一个根节点和两棵互不相交的,分别称为根节点的左子树和右子树的二叉树组成。
满二叉树:在一棵二叉树中,如果所有的分支的节点都存在左子树和右子树并且所有叶子都在同一层上这样的二叉树称为满二叉树。
满二叉树的特点有:
-叶子只能出现在最下一层。
-非叶子节点的度一定是2。
-在同样深度下的二叉树中,满二叉树的节点个数一定最多,同样叶子也是最多。
完全二叉树:
对一棵具有n个节点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的节点位置完全相同,则称为完全二叉树。
完全二叉树的特点有:
-叶子节点只能出现在最下两层。
-最下层的叶子一定集中在左部连续的位置。
-倒数第二层,若有叶子结点,一定出现在右部连续位置。
-若结点度为1,则该结点只有左孩子。
-同样结点数的二叉树,完全二叉树深度最小。
满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树。
二叉树的特点:
-在二叉树的第i层上至多有2(i-1)个结点(i>=1)
-深度为k的二叉树至多有2k-1个结点(k>=1)
-对于任何一棵二叉树T,如果其终端结点(度为0)数为n0,度为2的结点数为n2,则n0=n2+1。
-具有n个结点的完全二叉树的深度为log2n向下取整再加1。
二叉树的链式存储结构:
typedef struct BiTNode
{
Object data;
BiTNode *leftChild;
BiTNode *rightChild;
} BiTNode,*BiTree;
二叉树的遍历:
前序遍历:若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树。
中序遍历:若二叉树为空,则空操作返回,否则从根节点开始(并不是先访问根节点),中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树。
后序遍历:
若二叉树为空,则空操作返回。否则从左到右先叶子后节点的方式遍历访问左右子树,最后访问根节点。
层序遍历:
若二叉树为空,则空操作返回。否则从树的第一层开始访问,从上到下逐层遍历,在同一层中从左到右的顺序对节点逐个访问。
二叉树建立结点为什么用双重指针?
如果只用指针,作形参传给建立结点的函数,这个指针值传给了函数栈中的内存,函数返回后,函数栈销毁,不能获得结点
而用指针的指针,函数内修改了这个双重指针指向的值(即结点指针),在函数外也能获得结点。