数据结构与算法:PART1
算法时间分析: https://www.acwing.com/blog/content/32/%E5%A5%BD%E4%B8%9C%E8%A5%BF
理解idx: 我感觉idx相当于一个分配器,如果需要加入新的结点就用++idx分配出一个下标(最主要可以做到不重复地重新分配下标)
AcWing 835. 如何理解单(双)链表,Trie树和堆中的idx? - AcWing
所有数据结构 学习的同时一定要理解该种数据结构的使用范围,干什么的/
科班学习顺序:(如何逐步提高写程序的性能) 基础程序设计->数据结构与算法->操作系统->编译器的优化(编译原理)
提高程序性能办法: 好的算法:正确性,可读性,健壮性,效率
程序运行时间因素
所用算法
问题的规模
书写程序所用语言->级别越高效率越低
编译程序所用的机器(mac比windows快)
机器执行所用的速度(涉及到硬件,比如老电脑和新电脑)
算法时间度量:为了完成某一问题机器所做的操作执行次数
统计方法:写代码前/写代码后
1 2 3 4 5 6 7 int i, sum=0 , n=100 ;for (i=1 ;i<=n;i++){ sum+=i; } cout<<sum;
冯诺依曼架构 I/O<->中央存储单元<->
CPU:解析指令
内存:存储指令和数据
程序大小相对不重要,执行操作与数据重要
空间复杂度: 通过计算算法所创建的空间大小。
基础算法 位运算 1 2 求n的第k位数字: n >> k & 1 返回n的最后一位1 :lowbit (n) = n & -n
快排——》重要,面试常用型😍 快排本质就是使用分治思想,递归实现
最快onlogn,最慢oN2
步骤:
确定分界点
调整范围(最麻烦部分)
对左右两边进行操作
模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void quick_sort (int q[], int l, int r) { if (l >= r) return ; int i = l - 1 , j = r + 1 , x = q[l + r >> 1 ]; while (i < j) { do i ++ ; while (q[i] < x); do j -- ; while (q[j] > x); if (i < j) swap (q[i], q[j]); } quick_sort (q, l, j), quick_sort (q, j + 1 , r); }
使用说明:
1 2 3 如原本为a[10 ],数据为a[0 ]-a[9 ]; 则排序为 quick_sort (a, 0 , 9 );
由于使用do-while循环,所以i和j一定会!!!自增!!!,使得循环会继续下去,但是如果采用while循环(i和j的初始化做出对应的变更),i和j在特殊情况下不自增的话,循环就会卡死
边界问题:
快排属于分治算法,最怕的就是 n分成0和n,或 n分成n和0,这会造成无限划分
1 2 3 while (q[i] < x) i++; while (q[j] > x) j--; 当q[i]和q[j]都为 x 时, i 和 j 都不会更新,导致 while 陷入死循环
单向移动版本快排(实际使用快慢指针) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int partition (int arr[], int low, int high) { int pivot = arr[low]; int i = low; for (int j = low + 1 ; j <= high; j++) { if (arr[j] < pivot) { i++; swap (arr[i], arr[j]); } } swap (arr[i], arr[low]); return i; } void quickSort (int arr[], int low, int high) { if (low < high) { int pi = partition (arr, low, high); quickSort (arr, low, pi - 1 ); quickSort (arr, pi + 1 , high); } }
运行逻辑:指针j运行快,i运行慢,j只会在遇到比基准元素大的时候跳过
正常流程:如果j指向的都是比pivot小的元素,ji同步运动,指针一直向右走
如果j右边是比pivot大的元素,即i右边紧挨着就是更大的元素,j跳过,i停留不移动
然后让i++,刚好就到了大的元素,进行交换
结束情况:j遍历完成,i最后右移一次,停止
接着就是分治了
插入排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void insertionSort (int arr[], int n) {int i, key, j; for (i = 1 ; i < n; i++) { key = arr[i]; j = i - 1 ; while (j >= 0 && arr[j] > key) { arr[j + 1 ] = arr[j]; j = j - 1 ; } arr[j + 1 ] = key; } }
plus:折半插入排序(插入+二分)——》稳定又好使 代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int binarySearch (int a[], int item, int low, int high) { if (high <= low) { return (item > a[low]) ? (low + 1 ) : low;} int mid = (low + high) / 2 ; if (item == a[mid]) { return mid + 1 ;} if (item > a[mid]) { return binarySearch (a, item, mid + 1 , high);} return binarySearch (a, item, low, mid - 1 ); } void insertionSort (int a[], int n) { int i, loc, j, k, selected; for (i = 1 ; i < n; ++i) { j = i - 1 ; selected = a[i]; loc = binarySearch (a, selected, 0 , j); while (j >= loc) { a[j + 1 ] = a[j]; j--; } a[j + 1 ] = selected; } }
希尔排序 模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int shellSort (int arr[], int n) {for (int gap = n / 2 ; gap > 0 ; gap /= 2 ) { for (int i = gap; i < n; i += 1 ) { int j; for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = temp; } } return 0 ; }
归并排序(另一个重要排序,也背下来)😃 不太好的点:归并排容易爆内存
模板: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void merge_sort (int q[], int l, int r) { if (l >= r) return ; int mid = l + r >> 1 ; merge_sort (q, l, mid); merge_sort (q, mid + 1 , r); int k = 0 , i = l, j = mid + 1 ; 分别作用: k用于tmp数组,因此从0 开始 i从最左,j从中间开始向右 while (i <= mid && j <= r) if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ]; else tmp[k ++ ] = q[j ++ ]; while (i <= mid) tmp[k ++ ] = q[i ++ ]; while (j <= r) tmp[k ++ ] = q[j ++ ]; for (i = l, k = 0 ; i <= r; i ++, k ++ ) q[i] = tmp[k]; k代表临时数组的值 }
使用举例:
几个注意点:
tmp一开始就声明(不然爆内存)
merge里面j=mid+1(这里弄错之后排序都不对劲)
merge函数中i与j的范围记得别被错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> using namespace std;const int N=1e5 +10 ;int array[N];int tmp[N];void merge (int l,int r) { int mid=(l+r)/2 ; int k=0 ,i=l,j=mid+1 ; while (i<=mid&&j<=r) { if (array[i]<=array[j])tmp[k++]=array[i++]; else tmp[k++]=array[j++]; } while (i<=mid){tmp[k++]=array[i++];} while (j<=r){tmp[k++]=array[j++];} for (i=l,j=0 ;i<=r;i++,j++) { array[i]=tmp[j]; } } void mergesort (int l,int r) { if (l>=r)return ; int i=l-1 ; int j=r+1 ; int mid=(i+j)/2 ; mergesort (l,mid); mergesort (mid+1 ,r); merge (l,r); } int main () { int n; cin>>n; for (int i=0 ;i<n;i++) { cin>>array[i]; } mergesort (0 ,n-1 ); for (int i=0 ;i<n;i++) { cout<<array[i]<<' ' ; } return 0 ; }
有趣应用:逆序对 788. 逆序对的数量 - AcWing题库
timsort(优化后归并) 这个就看苏老师的ppt课件
(105条消息) Timsort——自适应、稳定、高效排序算法_码到sucess的博客-CSDN博客_timsort
核心:提取降序数组升级为升序 数组本质都是部分有序的,
因此第一步:将所有部分降序数组全部翻转(这里直接逆序就好)
这一步模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void reverse (int q[],int l,int r) { for (int i=l,j=r;i<=j;i++,j--)swap (q[i],q[j]); } void array_reverse (int q[],int N) { if (N==1 &&N==0 )return ; int i=1 ,l=0 ,tmp=0 ,stage_judge; if (q[0 ]<=q[1 ])stage_judge=1 ; else stage_judge=0 ; while (i<N) { if (q[i]>q[i+1 ]) { if (stage_judge==1 )stage_judge=0 ; ++i; } else if (q[i]<=q[i+1 ]) { if (stage_judge==0 ) { if (tmp!=l-1 )++l; reverse (q,l,i); tmp=i; stage_judge=1 ; } ++i; l=i; } } }
核心:栈处理归并 先将所有已知的升序排列的压入总栈,
这一步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void stack_check (int N) { int stack_pos1,stack_pos2,stack_pos3; int stack_length1,stack_length2,stack_length3; pop (stack_pos1,stack_length1); pop (stack_pos2,stack_length2); pop (stack_pos3,stack_length3); if (stack_length2<stack_length1||stack_length3<stack_length1+stack_length2) { if (stack_length1>stack_length3) { push (stack_pos1,stack_length1); merge (stack_pos3, stack_length3,stack_pos2, stack_length2 ); } else { push (stack_pos3,stack_length3); merge (stack_pos2,stack_length2,stack_pos1,stack_length1); } for (int i=0 ;i<N;i++) { cout<<q[i]<<" " ; } cout<<endl; if (stack_idx>=2 )stack_check (N); } else { push (stack_pos3,stack_length3); push (stack_pos2,stack_length2); push (stack_pos1,stack_length1); cout<<endl<<endl; } return ; }
核心:最小分区长度在排序之前预先计算 ➢ 归并过程低效的主要原因是大分区和小分区合并
1 2 3 4 5 6 7 8 9 10 while (stack_idx>=1 ) { pop (stack_pos1,stack_length1); pop (stack_pos2,stack_length2); if (stack_pos1>stack_pos2)merge (stack_pos2,stack_length2,stack_pos1,stack_length1); else merge (stack_pos1,stack_length1,stack_pos2,stack_length2); for (int i=0 ;i<N;i++) { cout<<q[i]<<" " ; }
timsort模板(自己写的!) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 #include <iostream> using namespace std;const int M =10010 ;int q[M];int tmp[M];int pos_stack[M];int length_stack[M];int stack_idx=-1 ;void push (int pos,int length ) { pos_stack[++stack_idx]=pos; length_stack[stack_idx]=length; } void pop (int &tmp_pos,int &tmp_length) { tmp_pos= pos_stack[stack_idx]; tmp_length= length_stack[stack_idx]; --stack_idx; return ; } void reverse (int q[],int l,int r) { for (int i=l,j=r;i<=j;i++,j--)swap (q[i],q[j]); } void array_reverse (int q[],int N) { if (N==1 &&N==0 )return ; int i=1 ,l=0 ,tmp=0 ,tem_l=0 ,stage_judge; if (q[0 ]<=q[1 ])stage_judge=1 ; else stage_judge=0 ; while (i<N) { if (q[i]>q[i+1 ]) { if (stage_judge==1 ) { stage_judge=0 ; } ++i; } else if (q[i]<=q[i+1 ]) { if (stage_judge==0 ) { if (tmp!=l-1 )++l; reverse (q,l,i); tmp=i; stage_judge=1 ; } ++i; l=i; } } } void merge (int pos1,int length1,int pos2,int length2) { int r=pos2+length2-1 ; int l=pos1; int mid=pos1+length1-1 ; int k = 0 , i = l, j = pos2 ; while (i <= mid && j <= r) if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ]; else tmp[k ++ ] = q[j ++ ]; while (i <= mid) tmp[k ++ ] = q[i ++ ]; while (j <= r) tmp[k ++ ] = q[j ++ ]; for (i = l, j = 0 ; i <= r; i ++, j ++ ) q[i] = tmp[j]; push (pos1,length1+length2); } void stack_check (int N) { int stack_pos1,stack_pos2,stack_pos3; int stack_length1,stack_length2,stack_length3; pop (stack_pos1,stack_length1); pop (stack_pos2,stack_length2); pop (stack_pos3,stack_length3); if (stack_length2<stack_length1||stack_length3<stack_length1+stack_length2) { if (stack_length1>stack_length3) { push (stack_pos1,stack_length1); merge (stack_pos3, stack_length3,stack_pos2, stack_length2 ); } else { push (stack_pos3,stack_length3); merge (stack_pos2,stack_length2,stack_pos1,stack_length1); } for (int i=0 ;i<N;i++) { cout<<q[i]<<" " ; } cout<<endl; if (stack_idx>=2 )stack_check (N); } else { push (stack_pos3,stack_length3); push (stack_pos2,stack_length2); push (stack_pos1,stack_length1); cout<<endl<<endl; } return ; } int main () { int N; cin >> N; int stack_pos1,stack_pos2,stack_pos3; int stack_length1,stack_length2,stack_length3; for (int i = 0 ; i < N; i++) { cin >> q[i]; } array_reverse (q, N - 1 ); int l=0 ; for (int i = 0 ; i < N; i++) { if (q[i]>q[i+1 ]) { push (l,i-l+1 ); l=i+1 ; } } cout<<endl<<endl; for (int i=0 ;i<N;i++) { cout<<q[i]<<" " ; } cout<<endl; if (stack_idx>=2 )stack_check (N); while (stack_idx>=1 ) { pop (stack_pos1,stack_length1); pop (stack_pos2,stack_length2); if (stack_pos1>stack_pos2)merge (stack_pos2,stack_length2,stack_pos1,stack_length1); else merge (stack_pos1,stack_length1,stack_pos2,stack_length2); for (int i=0 ;i<N;i++) { cout<<q[i]<<" " ; } } return 0 ; }
二分 二分本质并不是单调性:有单调性一定可以二分解,可以二分解的题目不一定满足单调性,本质:可以将原本区间分成两个部分
二分一定有解(自己的二分的性质是一定有边界的),但题目可能会无解 (看例题)
整数二分比实数二分蛋疼很多 :整数有边界问题很恶心
当出现最小值最大(最右端)或最大值最小(最左端)或求最大值、最小值时,就可以考虑一下二分了
整数模板(两种) 应用:
1:找大于等于数的第一个位置 (满足某个条件的第一个数) 2:找小于等于数的最后一个数 (满足某个条件的最后一个数) 3.查找最大值 (满足该边界的右边界)、 4.查找最小值 (满足该边界的左边界)
然后每次使用这这两个模板的时候,先想是找这个区间的左端点还是还是右端点,然后选择用模板,最后再去写判断条件 。
设置红绿交界点是要求的位置
最后收敛到整个数组中满足条件的最右边的点
最后收敛到数组中满足条件的最左边的点
记忆方式:有减必有加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 bool check (int x) {} int SR (int l, int r) { while (l < r) { int mid = l + r + 1 >> 1 ; if (check (mid)) l = mid; else r = mid - 1 ; } return l; } int SL (int l, int r) { while (l < r) { int mid = l + r >> 1 ; if (check (mid)) r = mid; else l = mid + 1 ; } return l; }
经典:二分模板最终一定有解,题目不一定有解(最后的判断不满足题目)
解答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <algorithm> #include <iostream> using namespace std;const int N = 1e5 + 10 ;int q[N];int SL (int l, int r, int x) { while (l < r) { int mid = l + r >> 1 ; if (q[mid] >= x) r = mid; else l = mid + 1 ; } return l; } int SR (int l, int r, int x) { while (l < r) { int mid = l + r + 1 >> 1 ; if (q[mid] <= x) l = mid; else r = mid - 1 ; } return r; } int main () { int n,m; scanf ("%d%d" ,&n,&m); for (int i=0 ;i<n;++i) scanf ("%d" ,&q[i]); while ( m-- ) { int x; scanf ("%d" ,&x); int l = SL (0 , n - 1 , x); if (q[l]!=x) cout <<"-1 -1" <<endl; else { cout << l << ' ' ; cout << SR (0 , n - 1 , x) << endl; } } return 0 ; }
同样的例题:(爷跟你拼了)
照样使用二分(虽然是比较复杂的二分):二分,贪心 O(NlogL)
思路: 使得选手们在比赛过程中的最短跳跃距离尽可能长,当出现最小值最大(最右端)或最大值最小(最左端)或求最大值、最小值时,就可以考虑一下二分了 。验证答案具有单调性:拿走的石头越多,最短跳跃距离越大 ,这就叫答案的单调性
核心原理:
二分答案 二分答案就是把一组数据每次分成两部分,就是把大问题转化成小问题。例如猜数游戏,猜1-100的一个数,就先猜50,若小了,就猜75,若大了,就猜25,就这样一直猜下去,最终找到答案。而我们每一次猜的这个答案就是所求范围内的数据的中间数据,这就是二分答案。这个二分的中间数据就是指要求的内容 。
如果长度 LenLen 可以满足,那么当长度小于 LenLen 时也可以满足,所以我们可以二分出最大的 LenLen。 也就是在所有可满足的Len中寻找最右端的答案(这里指从0到最大),因此使用模板SR(再看上一道题实际上一个原理)
剩下的问题是如何判断给定 LenLen 的情况下,能否最多拿走 M块石头,使得所有相邻两块石头之间的距离不小于 LenLen。这一步可以贪心来做。从前往后扫描,并记一下上一块石头的位置 。
如果当前石头和上一块石头的距离小于 LenLen,则将当前石头拿走
如果当前石头和上一块石头的距离大于等于 LenLen,则将上一块石头更新成当前这块。 (和上一条是贪心时候的两种解法)
这里给出贪心证明:如果某个最优解中是拿走了上一块石头,那么我们可以改成留下上一块石头,拿走当前这块石头,这样总共拿走的石头数量不变,所以当前选法也是一组最优解。
check函数:我们遍历一遍每一块石头,累计出有多少块石头之间的间隔<=mid,如果超过m个,就不合法,如果小于等于m,就合法。 (累计间隔小也就是需要搬走多少个石头)
扫描结束后判断拿走的石头数量是否小于等于 M。(判断这个答案能不能执行,能说明答案mid还能猜测更大,更新l,不能说明mid猜测过大了,更新r )
时间复杂度分析 总共二分 O(logL)O(logL) 次,每次贪心的计算是 O(N)O(N),因此总时间复杂度是 O(NlogL)O(NlogL)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 50005 ;int a[N];int n,m,s;bool check (int x) { int cnt=0 ,last=0 ; for (int i=1 ;i<=n;i++) { if (a[i]-last<x) cnt++; else last=a[i]; } if (cnt>m) return false ; else return true ; } int main () { cin>>s>>n>>m; for (int i=1 ;i<=n;i++) cin>>a[i]; a[n+1 ]=s; n=n+1 ; int l=1 ,r=s; while (l<r) { int mid=l+r+1 >>1 ; if (check (mid)) l=mid; else r=mid-1 ; } cout<<l<<endl; }
https://www.acwing.com/blog/content/21312/
https://www.acwing.com/solution/content/118815/
https://leetcode.cn/leetbook/read/illustration-of-algorithm/59bjss/
https://www.acwing.com/solution/content/120802/
浮点模板 浮点好处在于不用考虑整数二分中的边界问题,因此直接用
1 2 3 4 5 6 7 8 9 10 11 12 13 bool check (double x) {} double bsearch_3 (double l, double r) { const double eps = 1e-6 ; while (r - l > eps) { double mid = (l + r) / 2 ; if (check (mid)) r = mid; else l = mid; } return l; }
例题:790. 数的三次方根 - AcWing题库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> #include <cstdio> using namespace std;double x;int main () { cin >> x; double l = -1000 ,r = 1000 ; while (r - l >= 1e-7 ) { double mid = (l + r) / 2 ; if (mid * mid * mid <= x) l = mid; else r = mid; } printf ("%.6lf" ,l); return 0 ; }
二分改进: 插值查找 应用范围:表长较大,关键字分布均匀
关键不同: 二分mid固定为0.5,插值查找mid参数动态变化
1 mid = low + (high - low) * (key - arr[low]) / (arr[high] - arr[low]) ;
斐波那契查找 (104条消息) 七大查找之斐波那契查找_非常规自我实现的博客-CSDN博客_fibonacci查找
前缀和(应用广泛) 数据结构应用:字符串哈希表kmp,自动机kmp,
前缀和+哈希表=LZW压缩->文本压缩 压缩部分: 解压缩部分: 离散化:整数离散化 如果使用哈希,会有额外的空间开销
特点:值域范围大,但是个数少(比如要访问到数组的10^9的位置,但个数只有10^5)也就是对于一个函数a->b,实际上只需要映射a,不需要操作b(当然高级玩家也可以继续弄b )。一般有两个问题:
a数组里面可能有重复元素,因此需要去重 ,(去重是最重要的)
如何算出a[i]离散化后的值,保序离散化 (a数组本身下标有序的),映射后一定也要是有序的,a[i].由于a有序,可以使用二分
需要使用的知识点:
离散化模板:(c++版本) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 vector<int > alls; sort (alls.begin (), alls.end ()); alls.erase (unique (alls.begin (), alls.end ()), alls.end ()); int find (int x) { int l = 0 , r = alls.size () - 1 ; while (l < r) { int mid = l + r >> 1 ; if (alls[mid] >= x) r = mid; else l = mid + 1 ; } return r + 1 ; }
AcWing 802. 区间和 - AcWing
分析:
使用离散化原因:
存储下标过大,不能开这么大的下标
使用数轴,会存在负值,不能使用下标
哈希表不能像离散化缩小数组的空间,可能导致遍历-e9~1e9 。此处的含义就是假如我们需要计算1e-9和1e9区间内的值,那我们需要从前到后枚举,无论该值是否存在。因为哈希表不能排序 ,因此不能提前知道哪些数周上的点不存在,会枚举多次(如最后query的时候,从1到1e9,使用哈希表就要遍历才知道是否有点,时间开销太大),
离散化本质:映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量 ,也就是如何能够将不连续的点映射到连续的数组的下标。
本题解法:(离散化) O((n+2∗m)log(n+2∗m))
开辟额外数组存放原来 的下标标志
对原来的数轴下标进行排序再去重,原因:考虑前缀和思想,我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,所以提前加入了这些元素,从而导致可能出现重复元素
最多使用n+2m次操作,最多使用的下标跨度为3*10^5,
首先用而二分写好映射后对应的数组下标,复杂度log(n + 2 * m)
1 2 3 4 5 6 7 8 9 10 11 int find (int x) { int l = 0 , r = alls.size () - 1 ; while (l < r) { int mid = l + r >> 1 ; if (alls[mid] >= x) r = mid; else l = mid + 1 ; } return r + 1 ; }
全解:
关于unique和erease可以看
[(100条消息) C++ 之vector元素去重unique()_sandalphon4869的博客-CSDN博客_unique vector](https://blog.csdn.net/sandalphon4869/article/details/98209093?ops_request_misc=%7B%22request%5Fid%22%3A%22166674706816800184111752%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=166674706816800184111752&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-98209093-null-null.142^v59^pc_rank_34_1,201^v3^control_1,213^v1^t3_control1&utm_term=vector unique&spm=1018.2226.3001.4187)
auto:(100条消息) c++ auto基本用法_lwgkzl的博客-CSDN博客_auto用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <iostream> #include <vector> #include <algorithm> using namespace std;typedef pair<int , int > PII;const int N = 300010 ;int a[N], s[N];int n, m;vector<int > alls; vector<PII> add, query; int find (int x) { int l = 0 , r = alls.size () - 1 ; while (l < r) { int mid = l + r >> 1 ; if (alls[mid] >= x) r = mid; else l = mid + 1 ; } return r + 1 ; } vector<int >:: iterator unique (vector<int > &a) { int j = 0 ; for (int i = 0 ; i < a.size (); i ++) if (!i || a[i] != a[i - 1 ]) a[j ++ ] = a[i]; return a.begin () + j; } int main () { cin >> n >> m; for (int i = 0 ; i < n; i ++ ) { int x, c; cin >> x >> c; add.push_back ({x, c}); alls.push_back (x); } for (int i = 0 ; i < m; i ++ ) { int l, r; cin >> l >> r; query.push_back ({l, r}); alls.push_back (l); alls.push_back (r); } sort (alls.begin (), alls.end ()); vector<int >::iterator pos = unique (alls); alls.erase (pos, alls.end ()); for (auto item : add) { int x = find (item.first); a[x] += item.second; } for (int i = 1 ; i <= alls.size (); i ++ ) s[i] = s[i - 1 ] + a[i]; for (auto item : query) { int l = find (item.first), r = find (item.second); cout << s[r] - s[l - 1 ] << endl; } return 0 ; } 3 3 1 2 3 6 7 5 1 3 4 6 7 8
补充auto:
1 2 3 4 5 6 7 8 9 10 11 int main () { vector<int >v; v.push_back (1 ); v.push_back (2 ); v.push_back (3 ); for (auto i : v){ cout<<i<<" " ; } cout<<endl; return 0 ; }
区间合并:不同于离散化 应用:很多区间,如果有交集就合并成一个更长的区间
区间合并算法:快速地进行多个区间的合并,当然可以进行一些特殊处理,比如对于端点就进行统一归并
步骤:
按区间左端点进行排序
扫描过程中,对于所有有交集的区间进行合并。
左边端点设置成start,右边设置成end,可能有的关系:
左右都在内部:原本区间不变
仅一部分在内部:新的ed会边长(左端点不会更新,因为是按照左端点从小到大的顺序进行区间的扫描的 )
都不在内部:不用管就完事了
代码模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <algorithm> void merge (vector<PII> &segs) { vector<PII> res; sort (segs.begin (), segs.end ()); int st = -2e9 , ed = -2e9 ; for (auto seg : segs) if (ed < seg.first) { if (st != -2e9 ) res.push_back ({st, ed}); st = seg.first, ed = seg.second; } else ed = max (ed, seg.second); if (st != -2e9 ) res.push_back ({st, ed}); segs = res; }
y总:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <bits/stdc++.h> using namespace std;typedef pair<int ,int > PII;vector<PII> segs; void merge (vector<PII>&segs) { vector<PII> res; sort (segs.begin (),segs.end ()); int l = -2e9 ,r = -2e9 ; for (auto item:segs) { if (r < item.first) { if (l != -2e9 ) res.push_back ({l,r}); l = item.first; r = item.second; } else r = max (r,item.second); } if (l != -2e9 ) res.push_back ({l,r}); segs = res; } int main () { int n; cin >> n; while (n--) { int l,r; scanf ("%d%d" ,&l,&r); segs.push_back ({l,r}); } merge (segs); cout << segs.size () << endl; return 0 ; }
数据结构 链表 有限性:数据元素个数有穷
相同性:数据元素的类型是同一的
顺序性:相邻的数据元素之间存在序偶关系
前言:
ps:算法题中大部分操作都是头插法
链表制造方式
结构体+指针(c++需要使用new,费时间)
数组模拟(这里是静态数组,用空间换时间)
小tips:邻接表本质就是n个单链表拼起来 ,正常一个head对应一条链表,这个就是开了一个组,head[i]对应第i条链表
数组模拟链表
单链表:邻接表(实际上n个链表),主要应用在于存储图和树
双链表:优化某些问题
单链表制作 826. 单链表 - AcWing题库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 const int n =1000010 ;int head,e[n],ne[n],idx;void initial () { head=-1 ; idx=0 } void add_to_tail (int x){ e[idx]=x; if (head==-1 { ne[idx]=head; head=idx; idx++; return ; } int i=head; if (head!=-1 )while (ne[i]!=-1 )i=ne[i]; ne[idx]=-1 ; ne[i]=idx; idx++; } void add_to_head (int x){ e[idx]=x; ne[idx]=head; head=idx; idx++; } void add (int k,int x){ k=k-1 ; e[idx]=x; ne[idx]=ne[k]; ne[k]=idx; idx++; } void delete (int k){ k=k-1 ; ne[k]=ne[ne[k]]; } void dele (int a) { if (e[head]==a)head=ne[head]; else for (int i=head;ne[i]!=-1 ;i=ne[i]) { if (e[ne[i]]==a) { ne[i]=ne[ne[i]]; } } } void print (){ for (int i=head;i!=-1 ;i=ne[i]) { cout<<e[i]<<' ' ; cout<<endl; } }
双链表制作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #include <iostream< using namespace std;const int M=20010 ;int r[M],prev[M],e[M],index;void initial () { r[0 ]=1 ; l[1 ]=0 ; index=2 ; } void add_to_right (int x,int k) { e[idx]=x; r[idx]=r[k]; l[idx]=k; l[r[k]]=idx; r[k]=idx idx++; } add_to_right ( x, k+1 ) add_to_right ( x, l[k+1 ]) add_to_right ( x, 0 ) add_to_right ( x, l[1 ]) void dele (int k) { r[l[k]]=r[k]; l[r[k]]=l[k]; } void print () { for (int i=r[0 ];i!=1 ;i=r[i]) { cout<<e[i]<<' ' ; cout<<endl; } } add_to_right ( x, k+1 ) add_to_left (x,k+1 ) dele (k+1 ) 第k个插入的数左边插入:add_to_right (x,l[k+1 ]) 第k个插入的数右边插入:add_to_right (x,k+1 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <stdlib.h> #include <stdio.h> struct Node { char data; struct Node * next; struct Node * pre; }; struct Node *p;struct Node *head;struct Node *tail;void Home () { p=head; } void End () { p=tail->pre; } void Left () { if (p==head) return ; p=p->pre; } void Right () { if (p->next==tail) return ; p=tail->pre; } void Back () { if (p==head) return ; struct Node *tmp=(struct Node*)malloc (sizeof (struct Node)); tmp=p; tmp->pre->next=tmp->next; tmp->next->pre=tmp->pre; free (tmp); } int main () { char s[50010 ]; int i; struct Node *head=(struct Node*)malloc (sizeof (struct Node)); struct Node *tail=(struct Node*)malloc (sizeof (struct Node)); struct Node *p=head; head->next=tail; tail->pre=head; tail->next=NULL ; head->pre=NULL ; scanf ("%s" ,s); for (i=0 ;s[i]!='\0' ;i++) { if (s[i]=='{' ) Home (); else if (s[i]=='}' ) End (); else if (s[i]=='<' ) Left (); else if (s[i]=='>' ) Right (); else if (s[i]=='#' ) Back (); else { struct Node*q=(struct Node*)malloc (sizeof (struct Node)); q->data=s[i]; q->pre=p; q->next=p->next; p->next->pre=q; p->next=q; p=p->next; } } for (p=head->next;p!=tail;p=p->next) { printf ("%c" ,p->data); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <iostream> #include <string> using namespace std;const int M=100090 ;int nex[M];int pre[M];int valu[M];int idx;void initial (void ) { nex[0 ]=1 ; pre[1 ]=0 ; idx=2 ; } void add_to_right (int x,int k) { valu[idx]=x; nex[idx]=nex[k]; pre[idx]=k; pre[nex[k]]=idx; nex[k]=idx; idx++; } void dele (int k) { nex[pre[k]]=nex[k]; pre[nex[k]]=pre[k]; } int main () { int tem; int index; int n; cin>>n; initial (); for (int i=0 ;i<n;i++) { string a; cin>>a; if (a=="D" ) { cin>>index; dele (index+1 ); } if (a=="L" ) { cin>>tem; add_to_right (tem,0 ); } if (a=="R" ) { cin>>tem; add_to_right (tem,pre[1 ]); } if (a=="IL" ) { cin>>index>>tem; add_to_right (tem,pre[index+1 ]); } if (a=="IR" ) { cin>>index>>tem; add_to_right (tem,index+1 ); } } for (int i=nex[0 ];i!=1 ;i=nex[i]) { cout<<valu[i] <<" " ; } return 0 ; }
链表递归与双指针 应用:力扣19题
方法:未知链表长度情况下通过递归获取长度
注意这种递归本质上是逆序进行扫描,从null扫描到head(不是空节点头,而是存放第一个值的头)
读取顺序讲解:
以1234,2示例如有a,b,c,d四个节点,分别存放1,2,3,4(a是头4是尾,4->next=NULL)
那么调用函数length时候会先一直向下扫描(因为未满足条件之前不会进行return),扫描到d的下一个节点null这时返回1->到达节点d,(因为有pos=length(node->next,n)+1,+1导致从节点null返回d时候pos变成1 )返回2(到达节点c),返回3(到达节点b),注意这时候满足倒序扫描到第三个了,也就是要删除的节点再向上回溯了1位 ,因此删除节点c,即node->next=node->next->next**(b指向c变成b指向d)**
好处:时间少(只用扫描一次),空间少(没有额外开辟指针)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {public : int length (ListNode *node,int n) { if (node==NULL )return 0 ; int pos=length (node->next,n)+1 ; if (pos==n+1 ) node->next=node->next->next; return pos; } ListNode* removeNthFromEnd (ListNode* head, int n) { int pos =length (head,n); if (pos==n)return head->next; return head; } };
参考网址:【反转链表】:双指针,递归,妖魔化的双指针 - 反转链表 - 力扣(LeetCode)
同样利用递归实现链表逆序:(有那么一点点费脑子) 原理:链表自身带有递归属性(一个大问题可以拆解成小问题)
将链表拆分成头节点和剩余节点,同理继续拆解,一直拆解到最后的尾节点和前面的一堆“头”节点,最后一个节点不需要进行翻转
这里就是对于子问题,将子链表进行翻转,就可以得到整个链表的反转,也就是递归的第一步
1 2 3 4 5 public ListNode reverseList (ListNode head) { ListNode newHead = reverseList (head.next); }
这里假设后续子链表已经全部完成翻转,那么只需要对“头节点”完成翻转
也就是
1 2 head->next->neat=head; head->next=NULL ;
完善之后就是有
1 2 3 4 5 6 7 8 9 10 public ListNode reverseList (ListNode head) { ListNode newHead = reverseList (head.next); head.next.next = head; head.next = null; return newHead; }
再加上约束条件(递归终止条件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : ListNode* reverseList (ListNode* head) { if (head == NULL || head->next == NULL ) { return head; } ListNode* ret = reverseList (head->next); head->next->next = head; head->next = NULL ; return ret; } };
双指针实现逆序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : ListNode* reverseList (ListNode* head) { ListNode* cur = NULL , *pre = head; while (pre != NULL ) { ListNode* temp = pre->next; pre->next = cur; cur = pre; pre = temp; } return cur; } };
原理:一开始创建两个指针pre和cur,pre指向head,cur指向null,tem有点类似指针交换数值中的tem,指向正常链表顺序的下一个节点(这里就是head->next),
有序链表的拼接(又是递归/bushi) 时间复杂度:O(m+n)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : ListNode* mergeTwoLists (ListNode* l1, ListNode* l2) { if (l1 == NULL ) { return l2; } if (l2 == NULL ) { return l1; } if (l1->val <= l2->val) { l1->next = mergeTwoLists (l1->next, l2); return l1; } l2->next = mergeTwoLists (l1, l2->next); return l2; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 解释的一个部分 关于return L1的个人理解: 递归的核心在于,我只关注我这一层要干什么,返回什么,至于我的下一层(规模减1 ),我不管,我就是甩手掌柜. 好,现在我要merge L1,L2.我要怎么做? - 显然,如果L1空或L2空,我直接返回L2或L1就行,这很好理解. - - 这个包裹是下级员工干的活,即merge (L1->next, L2) 我该返回啥? - 现在不管我的下一层干了什么,又返回了什么给我, 我只要知道,假设我的工具人们都完成了任务, 那我的任务也就完成了,可以返回最终结果了 - 最终结果就是我一开始接手的L1头结点+下级员工给我的大包裹,要一并交上去, 这样我的boss才能根据我给它的L1头节点往下找,检查我完成的工作
回文链表判断:(映射,递归,翻转) 映射到数组上再对数组进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : bool isPalindrome (ListNode* head) { vector<int > vals; while (head != nullptr ) { vals.emplace_back (head->val); head = head->next; } for (int i = 0 , j = (int )vals.size () - 1 ; i < j; ++i, --j) { if (vals[i] != vals[j]) { return false ; } } return true ; } };
优雅递归
遍历节点的方式可是我看不懂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { ListNode* frontPointer; public : bool recursivelyCheck (ListNode* currentNode) { if (currentNode != nullptr ) { if (!recursivelyCheck (currentNode->next)) { return false ; } if (currentNode->val != frontPointer->val) { return false ; } frontPointer = frontPointer->next; } return true ; } bool isPalindrome (ListNode* head) { frontPointer = head; return recursivelyCheck (head); } };
解释:currentNode 指针是先到尾节点,由于递归的特性再从后往前进行比较。frontPointer 是递归函数外的指针。若 currentNode.val != frontPointer.val 则返回 false。反之,frontPointer 向前移动并返回 true。
复杂度分析
时间复杂度:O(n)O(n),其中 nn 指的是链表的大小。 空间复杂度:O(n)O(n),其中 nn 指的是链表的大小。我们要理解计算机如何运行递归函数,在一个函数中调用一个函数时,计算机需要在进入被调用函数之前跟踪它在当前函数中的位置(以及任何局部变量的值),通过运行时存放在堆栈中来实现(堆栈帧)。在堆栈中存放好了数据后就可以进入被调用的函数。在完成被调用函数之后,他会弹出堆栈顶部元素,以恢复在进行函数调用之前所在的函数。在进行回文检查之前,递归函数将在堆栈中创建 nn 个堆栈帧,计算机会逐个弹出进行处理。所以在使用递归时空间复杂度要考虑堆栈的使用情况。 这种方法不仅使用了 O(n)O(n) 的空间,且比第一种方法更差,因为在许多语言中,堆栈帧的开销很大(如 Python),并且最大的运行时堆栈深度为 1000(可以增加,但是有可能导致底层解释程序内存出错)。为每个节点创建堆栈帧极大的限制了算法能够处理的最大链表大小。
快慢指针
很优雅**但是我照样看不懂***没想到吧老子看懂了哈哈哈哈哈哈哈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution {public : bool isPalindrome (ListNode* head) { if (head == nullptr ) { return true ; } ListNode* firstHalfEnd = endOfFirstHalf (head); ListNode* secondHalfStart = reverseList (firstHalfEnd->next); ListNode* p1 = head; ListNode* p2 = secondHalfStart; bool result = true ; while (result && p2 != nullptr ) { if (p1->val != p2->val) { result = false ; } p1 = p1->next; p2 = p2->next; } firstHalfEnd->next = reverseList (secondHalfStart); return result; } ListNode* reverseList (ListNode* head) { ListNode* prev = nullptr ; ListNode* curr = head; while (curr != nullptr ) { ListNode* nextTemp = curr->next; curr->next = prev; prev = curr; curr = nextTemp; } return prev; } ListNode* endOfFirstHalf (ListNode* head) { ListNode* fast = head; ListNode* slow = head; while (fast->next != nullptr && fast->next->next != nullptr ) { fast = fast->next->next; slow = slow->next; } return slow; } };
zhan(栈)不是B站那个 前言: 栈属于一种FILO数据结构,类似阉割版本的顺序表,在计算机中有非常广泛的应用
例:使用递归时,编译器自身对于递归指令的使用就是一种栈,或者计算中缀表达式(通过将中缀表达式转换为后缀表达式,再对后缀表达式进行栈运算就可以得出结果)->全部利用栈的FILO结构
栈制造方式
数组栈->非常非常easy
链表栈->参考链表,但是阉割
栈做非降路径问题 离散数学组合数学里面的东西,如果对一个栈仅允许出栈入栈操作,对输入固定长度字母求所有的输出模式,其实本质就是一个非降路径,只需要计算从(0,0)到(n,n)的不经过y=x+1的所有路径
中缀表达式转换后缀表达式 (104条消息) 中缀表达式转后缀表达式的方法_說詤榢的博客-CSDN博客_中缀表达式转后缀表达式
正则表达式分析 分析:
使用栈来求: 中缀表达式可以拆分成一棵中缀树,后缀类似拆分成后缀树,如(1+1)(2+2),中缀中根节点是 ,叶节点是+,叶子是1 1 2 2,如果用树的思想去做应该使用树的中序遍历,但是问题在于不好处理运算符的优先级问题,而改造成后缀表达式就会方便很多,如同上例子换成后缀表达式变成了1 1 + 2 2 + *,然后使用栈来进行运算,数字压栈,运算符出栈两个进行运算后再次压栈,这样就可以避开运算符优先级的处理问题(相对更好理解一些而且不需要搭建树,可以使用栈来搞 )
使用递归来求: 已知将该表达式抽象成为一棵树,那么对每个子树使用递归求值可以不断削减树的层数,最后获得根树的值(**很牛逼但是很麻烦,要提前接触到树**)
这里使用栈来进行一个中缀表达式的计算
先看下只有 + 和 * 的。
输入长度为n的字符串,例如:1+2+34 5
输出表达式的值,即:63
应该用什么数据结构?
栈。
应该先计算哪一步?
实际应该先计算1+2。
“表达式求值”问题,两个核心关键点:
(1)双栈,一个操作数栈,一个运算符栈;
(2)运算符优先级,栈顶运算符,和,即将入栈的运算符的优先级比较:
如果栈顶的运算符优先级低,新运算符直接入栈
如果栈顶的运算符优先级高,先出栈计算,新运算符再入栈
仍以1+2+34 5举例,看是如何利用上述两个关键点实施计算的。
首先,这个例子只有+和*两个运算符,所以它的运算符表是:
这里的含义是:
(1)如果栈顶是+,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;
(2)如果栈顶是+,即将入栈的是*,栈顶优先级低,直接入栈;
(3)如果栈顶是*,即将入栈的是+,栈顶优先级高,需要先计算,再入栈;
(4)如果栈顶是, 即将入栈的是 ,栈顶优先级高,需要先计算,再入栈;
有了运算符表,一切就好办了。
一开始,初始化好输入的字符串,以及操作数栈,运算符栈。
一步步,扫描字符串,操作数一个个入栈,运算符也入栈。
下一个操作符要入栈时,需要先比较优先级。
栈内的优先级高,必须先计算,才能入栈。
计算的过程为:
(1)操作数出栈,作为num2;
(2)操作数出栈,作为num1;
(3)运算符出栈,作为op;
(4)计算出结果;
(5)结果入操作数栈;
接下来,运算符和操作数才能继续入栈。下一个操作符要入栈时,继续比较与栈顶的优先级。
栈内的优先级低,可以直接入栈。
字符串继续移动。
又要比较优先级了。
栈内的优先级高,还是先计算(3*4=12),再入栈。
不断入栈,直到字符串扫描完毕。
不断出栈,直到得到最终结果3+60=63,算法完成。
总结
“表达式求值”问题,两个核心关键点:
(1)双栈,一个操作数栈,一个运算符栈;
(2)运算符优先级,栈顶运算符,和,即将入栈的运算符的优先级比较: 如果栈顶的运算符优先级低,新运算符直接入栈
如果栈顶的运算符优先级高,先出栈计算,新运算符再入栈
这个方法的时间复杂度为O(n),整个字符串只需要扫描一遍。
运算符有+-/()~^&都没问题,如果共有n个运算符,会有一个n n的优先级表。
正则表达式代码 代码:
上 代 码
虽然但是接下来这个代码是中缀计算的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <iostream> #include <stack> #include <string> #include <unordered_map> using namespace std;stack<int > num; stack<char > op; unordered_map<char , int > h{ {'+' , 1 }, {'-' , 1 }, {'*' ,2 }, {'/' , 2 } }; void eval () { int a = num.top (); num.pop (); int b = num.top (); num.pop (); char p = op.top (); op.pop (); int r = 0 ; if (p == '+' ) r = b + a; if (p == '-' ) r = b - a; if (p == '*' ) r = b * a; if (p == '/' ) r = b / a; num.push (r); } int main () { string s; cin >> s; for (int i = 0 ; i < s.size (); i++) { if (isdigit (s[i])) { int x = 0 , j = i; while (j < s.size () && isdigit (s[j])) { x = x * 10 + s[j] - '0' ; j++; } num.push (x); i = j - 1 ; } else if (s[i] == '(' ) { op.push (s[i]); } else if (s[i] == ')' ) { while (op.top () != '(' ) eval (); op.pop (); } else { while (op.size () && h[op.top ()] >= h[s[i]]) eval (); op.push (s[i]); } } while (op.size ()) eval (); cout << num.top () << endl; return 0 ; }
单调栈(瞳孔地震型题解)😢 使用单调递增栈
麻烦地方:超时
考虑方式有些类似双指针
思路:暴力 ->优化暴力
队列里面是否有元素没用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std;const int N=10010 ;int n;int stk[N],tt;int main () { cin>>n; for (int i=0 ;i<n;i++) { int x; cin>>x; while (tt&&stk[tt]>=x)tt--; if (tt)cout<<stk[tt]<<" " ; else cout<<-1 <<" " ; stk[++tt]=x; } return 0 ; }
队列 FIFO构造
队列构造方式: 数组构造(也是hin简单就是了)😒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const int M=200600 ;int bottom=0 ;int line[M]={0 };int top=0 ;void insert (int x) { line[top]=x; ++top; } void pop (void ) { bottom++; } void query (void ) { cout<<line[bottom]<<endl; return ; } void empty (void ) { if (top==bottom)cout<<"YES\n" ; else cout<<"NO\n" ; return ; }
循环队列: 模板:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int q[N], hh = 0 , tt = 0 ;q[tt ++ ] = x; if (tt == N) tt = 0 ;hh ++ ; if (hh == N) hh = 0 ;q[hh]; if (hh != tt){ }
单调队列~~~~~~~(~ ̄▽ ̄)~滑动窗口经典(配合单调栈食用) 😶🌫️准备好开始头疼
思路同单调栈:从暴力解决入手接着开始优化
可以使用队列对窗口进行维护(标准的入列和出列)
优化:队列中是否有没用的元素,对没用的元素进行删除看能否得到单调性 ,如3,-1,-3,在-3入列的时候就有3>-3,则最小值一定不会是3而且-3存在时间更久,因此使用单调栈的同样原理可以求出单调最小,并且是一个单调递增的最小
可以使用STL标准库来写或者说使用栈和队列数组模拟去写,而相对而言使用数组有很大的好处在于数组速度快,在比赛或者笔试时候会慢一些,在IDE中可能会有O2或者O3优化
再看一遍单调栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> using namespace std;const int N=10010 ;int n;int stk[N],tt;int main () { cin>>n; for (int i=0 ;i<n;i++) { int x; cin>>x; while (tt&&stk[tt]>=x)tt--; if (tt)cout<<stk[tt]<<" " ; else cout<<-1 <<" " ; stk[++tt]=x; } return 0 ; }
解题思路(以最大值为例):
由于我们需要求出的是滑动窗口的最大值。
如果当前的滑动窗口中有两个下标 i 和 j ,其中i在j的左侧(i<j),并且i对应的元素不大于j对应的元素(nums[i]≤nums[j]),则:
当滑动窗口向右移动时,只要 i 还在窗口中,那么 j 一定也还在窗口中。这是由于 i 在 j 的左侧所保证的。
因此,由于 nums[j] 的存在,nums[i] 一定不会是滑动窗口中的最大值了,我们可以将nums[i]永久地移除。
因此我们可以使用一个队列存储所有还没有被移除的下标,这里是q 。在队列中,这些下标按照从小到大的顺序被存储,并且它们在数组nums中对应的值是严格单调递减的。
当滑动窗口向右移动时,我们需要把一个新的元素放入队列中。
为了保持队列的性质,我们会不断地将新的元素与队尾的元素相比较,如果新元素大于等于队尾元素,那么队尾的元素就可以被永久地移除 ,我们将其弹出队列。我们需要不断地进行此项操作,直到队列为空或者新的元素小于队尾的元素。
由于队列中下标对应的元素是严格单调递减的,因此此时队首下标对应的元素就是滑动窗口中的最大值。
窗口向右移动的时候。因此我们还需要不断从队首弹出元素保证队列中的所有元素都是窗口中的,因此当队头元素在窗口的左边的时候,弹出队头。
每一个窗口的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 hh = 0 ; tt = -1 ; for (int i = 0 ; i < n; ++ i) { if (i - k + 1 > q[hh]) ++ hh; while (hh <= tt && a[i] >= a[q[tt]]) -- tt; q[++ tt] = i; if (i + 1 >= k) printf ("%d " , a[q[hh]]); }
判断最小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 int a[N]={0 };int q[N]={0 };int hh=0 ;int tt = -1 ;int n, k; cin >> n >> k; for (int i = 0 ; i < n; ++ i) { scanf ("%d" , &a[i]); if (i - k + 1 > q[hh]) ++ hh; while (hh <= tt && a[i] <= a[q[tt]]) -- tt; q[++ tt] = i; if (i + 1 >= k) printf ("%d " , a[q[hh]]); } cout << endl;
全部代码!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # include <iostream> using namespace std;const int N = 1000010 ;int a[N], q[N], tail = -1 ;int head=0 ;int main () { int n, k; cin >> n >> k; for (int i = 0 ; i < n; ++ i) { scanf ("%d" , &a[i]); if (i - k + 1 > q[head]) ++ head; while (head <= tail && a[i] <= a[q[tail]]) -- tail; q[++ tail] = i; if (i-k + 1 >= 0 ) printf ("%d " , a[q[head]]); } cout << endl; head = 0 ; tail = -1 ; for (int i = 0 ; i < n; ++ i) { if (i - k + 1 > q[head]) ++ head; while (head <= tail && a[i] >= a[q[tail]]) -- tail; q[++ tail] = i; if (i + 1 >= k) printf ("%d " , a[q[head]]); } return 0 ; }
串 KMP (100条消息) 数据结构KMP算法配图详解(超详细)_哈顿之光的博客-CSDN博客_kmp算法难吗是什么级别 (好好看好好学 )
模板y总 注意kmp算法的下标要从1开始弄!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 for (int i=1 ;i<=n;i++){ cin>>p[i]; } 或者邪教读取法:cin>>n>>p+1 >>m>>s+1 求模式串的Next数组: for (int i = 2 , j = 0 ; i <= m; i ++ ){ while (j && p[i] != p[j + 1 ]) j = ne[j]; if (p[i] == p[j + 1 ]) j ++ ; ne[i] = j; } for (int i = 1 , j = 0 ; i <= n; i ++ ){ while (j && s[i] != p[j + 1 ]) j = ne[j]; if (s[i] == p[j + 1 ]) j ++ ; if (j == m) { j = ne[j]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> using namespace std;const int N=100010 ,M=1000010 ;char p[N],s[M];int ne[N];int n,m;int main () { cin>>n>>p+1 >>m>>s+1 ; for (int i=2 ,j=0 ;i<=n;i++) { while (j&&p[i]!=p[j+1 ])j=ne[j]; if (p[i]==p[j+1 ])j++; ne[i]=j; } for (int i=1 ,j=0 ;i<=m;i++) { while (j&&s[i]!=p[j+1 ])j=ne[j]; if (s[i]==p[j+1 ])j++; if (j==n) { cout<<i-n<<' ' ; j=ne[j]; } } return 0 ; }
关键:特殊数组next的构造 ,前缀表
下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了
前缀表:字符串的最长 前缀:不包含尾字符的所有子串
后缀:相较于前缀,不包含首字母的所有子串
因此如果仅有单个字符则其前缀为0
特性:
目前讲解的构造方法
例:子串aabaabaaf,前缀表010120(aabaaf)
前缀形式:010120
全部后移方式:-1 0 10120
整体减一方式:-1 0 -1 0 1 -1
共同点:最后都应当保持封闭,虽然理解有出入但本质相同
代码随想录方法:前缀不减形式 prev,latter;
prev:前缀末尾位置 ,同时代表prev包括prev之前子串最长相等前后缀的长度,也表示前缀末尾
latter:后缀末尾位置
初始化
1 2 3 4 5 prev=0 ; latter=1 ; next[0 ]=0 ;
开始处理前后缀不同情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 for (latter=1 ;latter<slength;latter++){ while (s[prev]!=s[latter]&&prev>0 ) { prev=next[prev-1 ]; } 分界线:前后缀相同的情况 if (s[prev]==s[latter]) { prev++; } next[latter]=prev; }
这里则是全部操作的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void getNext (int * next, const string& s) { int j = 0 ; next[0 ] = 0 ; for (int i = 1 ; i < s.size (); i++) { while (j > 0 && s[i] != s[j]) { j = next[j - 1 ]; } if (s[i] == s[j]) { j++; } next[i] = j; } } int strStr (string haystack, string needle) { if (needle.size () == 0 ) { return 0 ; } int next[needle.size ()]; getNext (next, needle); int j = 0 ; for (int i = 0 ; i < haystack.size (); i++) { while (j > 0 && haystack[i] != needle[j]) { j = next[j - 1 ]; } if (haystack[i] == needle[j]) { j++; } if (j == needle.size () ) { return (i - needle.size () + 1 ); } } return -1 ; }
注意这里是文本串和模式串不匹配时候的操作(这里next数组采用正常前缀表)
数据结构作业出错原因 要求每一个都求出来
问题在于:当每一次满足条件后进行清零操作 ,本质上同一次两者不等时候的回溯操作(prev到上一位最接近的前缀位置) ,如果使用prev=0就会出现可能错过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 例: ababa aba 001 if (prev==sublength) { cout<<endl<<latter-sublength+1 <<endl; prev=next[prev-1 ]; } /错误 if (prev==sublength) { cout<<endl<<latter-sublength+1 <<endl; prev=0 ; }
automata~有限状态自动机 参考:
代码(构建自动机状态表) 1 2 3 4 5 6 7 8 9 10 11 12 dfa[0 ][P[0 ]] = 1 ; for (int c = 0 ; c < R; c++) { dfa[j][c] = dfa[X][c]; } dfa[j][P[j]] = j + 1 ; X = dfa[X][P[j]] }
聪明版本自动机 已知ascii码总共就128个,直接开一个大表就行
TRIE树(有些类似哈夫曼树编码) 类似但和哈夫曼树没有关系
又称字典树、单词查找树
应用:快速存储和查找字符串集合的数据结构
如何存储:构建串树 从根节点开始存储每一个字符,开始逐个向下进行创建,在单词的末尾打上一个标记表示该单词走到结尾了
TRIE树本质是一颗N叉树,有多少种字符一个节点就最多有多少条边
如何查找 从单词的首字母开始向下走,走到标记表示到头了
构建TRIE树 这里使用了数组模拟树的知识
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> using namespace std;const int N = 100010 ;int son[N][26 ];int cnt[N], idx; char str[N];void insert (char *str) { int p = 0 ; for (int i = 0 ; str[i]; i ++ ) { int u = str[i] - 'a' ; if (!son[p][u]) son[p][u] = ++ idx; p = son[p][u]; } cnt[p] ++ ; } int query (char *str) { int p = 0 ; for (int i = 0 ; str[i]; i ++ ) { int u = str[i] - 'a' ; if (!son[p][u]) return 0 ; p = son[p][u]; } return cnt[p]; } int main () { int n; scanf ("%d" , &n); while (n -- ) { char op[2 ]; scanf ("%s%s" , op, str); if (*op == 'I' ) insert (str); else printf ("%d\n" , query (str)); } return 0 ; }
TRIE树的其它应用 https://www.acwing.com/blog/content/32/%E5%A5%BD%E4%B8%9C%E8%A5%BF
启示:字典树不单单可以高效存储和查找字符串集合,还可以存储二进制数字
思路:利用二叉树 对所有的aiaj建立一个串数组,对于每一个固定的ai,每一次尽量与和当前不同的分支向下走,走到底这样一定就可以得到最优解
即顺序:所有值的二进制表示建立树->遍历一次,每一个ai进行寻找对应的最大的值,最后遍历一次之后获得答案
算法复杂度: (建立树)+n(每一个ai只需要在已经建立好的树再从头到尾走一次就好)
代码: insert函数改:
每个数看作一个31位长度的二进制数,最高位是0往0走,最高位1往1走,然后和类似TRIE的操作,但是构建的是一颗二叉树(一定概率变成斜树)
1 2 3 4 5 6 7 8 9 10 11 void insert (int x) { int p=0 ; for (int i=30 ;i>=0 ;i--) { int u=x>>i&1 ; if (!son[p][u]) son[p][u]=++idx; p=son[p][u]; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int search (int x) { int p=0 ;int res=0 ; for (int i=30 ;i>=0 ;i--) { int u=x>>i&1 ; if (son[p][!u]) { p=son[p][!u]; res=res*2 +1 ; } else { p=son[p][u]; res=res*2 +0 ; } } return res; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream> #include <algorithm> using namespace std;int const N=100010 ,M=31 *N;int n;int a[N];int son[M][2 ],idx;void insert (int x) { int p=0 ; for (int i=30 ;i>=0 ;i--) { int u=x>>i&1 ; if (!son[p][u]) son[p][u]=++idx; p=son[p][u]; } } int search (int x) { int p=0 ;int res=0 ; for (int i=30 ;i>=0 ;i--) { int u=x>>i&1 ; if (son[p][!u]) { p=son[p][!u]; res=res*2 +1 ; } else { p=son[p][u]; res=res*2 +0 ; } } return res; } int main (void ) { cin>>n; idx=0 ; for (int i=0 ;i<n;i++) { cin>>a[i]; insert (a[i]); } int res=0 ; for (int i=0 ;i<n;i++) { res=max (res,search (a[i])); } cout<<res<<endl; }
跳表——同样面试——但是不用手搓 一些参考的博客: (101条消息) 十分钟弄懂什么是跳表,不懂可以来打我_愤怒的可乐的博客-CSDN博客_跳表
优点:将链表查找的时间复杂度改造成log ,据说可以取代红黑树
跳表属于对链表 的改进
有点相似kmp?通过一些手段加快跳跃的速度
想法:链表中增加一些“超级链接”
特点:
跳表结合了链表和二分查找的思想
由原始链表和一些通过“跳跃”生成的链表组成
第0层是原始链表,越上层“跳跃”的越高,元素越少
上层链表是下层链表的子序列
查找时从顶层向下,不断缩小搜索范围
每一层增长哪一个节点实际上是随机生长的
核心思想:
跳表节点中用数组存储不同层级的下一个节点
随即生长
空间使用率最高的跳表
实现 构建过程: 初始化和单链表一样,仅有一个空节点,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private class Node { E data; List<Node> forwards; Node (E data) { this .data = data; forwards = new ArrayList<>(); for (int i = 0 ; i <= maxLevel; i++) { forwards.add (null); } } @Override public String toString () { return data == null ? " " : "" + data; } Node next (int level) { return this .forwards.get (level); } }
TRIE树的其它 散列表——哈希表-面试很重要 特点:查找与删除,查找全部在常数时间内完成
应用:
操作系统
数据库
编译器
计网
图像检索(最初始用于人脸识别等)
线性表总结:
哈希定义,应用 本质:给定一个输入给出一个唯一的序列号输出,将一个比较大的空间映射到一个比较小的空间,将一个复杂的数据结构映射到一个小的
应用举例:
输入n个数10^5,数的范围+-10^9,选择一些数字插入,选择另外一些数字查询
文本压缩和解压缩
定位的过程:元素通过哈希函数转换成唯一的整数(必须快速计算 )
第一步:将一个元素映射成一个整数
哈希模板(正常+字符串版本) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 (1 ) 拉链法 int h[N], e[N], ne[N], idx; const int N=1e5 +3 ; void insert (int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx ++ ; } bool find (int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1 ; i = ne[i]) {if (e[i] == x) return true ;} return false ; } (2 ) 开放寻址法 const int N=2e5 +3 ; int h[N]; const int null=0x3f3f3f3f ; int find (int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t ++ ; if (t == N) t = 0 ; } return t; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 核心思想:将字符串看成P进制数,P的经验值是131 或13331 ,取这两个值的冲突概率低 小技巧:取模的数用2 ^64 ,这样直接用unsigned long long 存储,溢出的结果就是取模的结果 typedef unsigned long long ULL;ULL h[N], p[N]; p[0 ] = 1 ; for (int i = 1 ; i <= n; i ++ ){ h[i] = h[i - 1 ] * P + str[i]; p[i] = p[i - 1 ] * P; } ULL get (int l, int r) { return h[r] - h[l - 1 ] * p[r - l + 1 ]; }
分配索引值 这里对字符进行处理,每一种字符都视为一种对应的数字,不同的位数有不同的加权值
1 2 3 4 5 6 7 8 9 10 unsigned int hash (char *key) { unsigned int hash_val = 0 ; while (*key != '\0' ) { hash_val = (hash_val << 5 ) + *key++; } return hash_val; }
缺点:虽然时间非常高效,但是空间浪费非常大(毕竟不能载满)
减少索引值: 方法1:忽略一部分元素,将另一部分直接视为索引值
好处:快,坏处:难于分配索引值
方法2:折叠,使用不同方式将原数据拆分,再合并在一起
方法3:余数运算,可以元素值除以某一特殊数字,余数用作索引值,
1 2 3 4 5 6 unsigned int hash (char *key, unsigned int H_SIZE) { unsigned int hash_val = 0 ; while (*key != '\0' ) { hash_val = (hash_val << 5 ) + *key++;} return hash_val % H_SIZE; }
字符串哈希——>字符串前缀哈希法 作用有些类似kmp,字符串也可以用哈希表做->一个集合到另一个集合的映射
实际上是字符串的前缀哈希法 ,对前缀进行哈希
问题:
如何定义某一个前缀的哈希值,可以将字符串视为p进制的一个数,每一位上的字母(acscii)视为对应的数字,但是不能映射成0 ->相同的字符串映射结果会相同->AA等等
哈希字符串假定人品足够不存在碰撞 ,没有考虑冲突情况,经验值:p取131或13331 时候,q取2^64,几乎99%情况不会出现冲突
好处:可以利用最前的哈希计算出所有子串的哈希 ,
已知h[r],h[l],
h[r]中r为第0位,h[l-1]中l-2为第0位
操作
h[l-1]与h[r]对齐,即向后移动多少位
h[r]-h[l-1]就能求出来了
小技巧:使用unsigned long long 存储所有h,相当于对所有数取模了 (因为溢出就相当于取模)
总结:左移高位对齐
之后对前缀全部处理完之后,就能用o1时间计算任意子串哈希值
预处理:h[i]=h[i-1]*p+str[i] (第i位字母)
牛逼的地方:比kmp牛逼:可以快速判断,快过o(n) ,是处理字符串的利器
代码核心部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef unsigned long long ULL;ULL h[N], p[N]; p[0 ] = 1 ; for (int i = 1 ; i <= n; i ++ ){ h[i] = h[i - 1 ] * P + str[i]; p[i] = p[i - 1 ] * P; } ULL get (int l, int r) { return h[r] - h[l - 1 ] * p[r - l + 1 ]; }
完整代码:
841. 字符串哈希 - AcWing题库
AcWing 841. 字符串哈希 【公式助理解】 - AcWing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <cstdio> #include <string> using namespace std;typedef unsigned long long ULL;const int N = 1e5 +5 ,P = 131 ;ULL h[N],p[N]; ULL query (int l,int r) { return h[r] - h[l-1 ]*p[r-l+1 ]; } int main () { int n,m; cin>>n>>m; string x; cin>>x; p[0 ] = 1 ; h[0 ] = 0 ; for (int i=0 ;i<n;i++){ p[i+1 ] = p[i]*P; h[i+1 ] = h[i]*P +x[i]; } while (m--){ int l1,r1,l2,r2; cin>>l1>>r1>>l2>>r2; if (query (l1,r1) == query (l2,r2)) printf ("Yes\n" ); else printf ("No\n" ); } return 0 ; }
哈希碰撞处理(面试高频)😍😍😍😍😍 可以把离散化看成一种特殊的哈希方式
哈希表属于期望算法 ,可以将哈希表的链长视为一个常数,
碰撞:两个相同的索引放在相同的索引位置
碰撞可能性很大->定义域很大值域比较小
拉链法(Open Hashing) 原理:如果多个索引值最终哈希值相同,使用链表的形式另外存储相同的值
添加,直接添加链
查找:对应位置在链表遍历一下
删除:算法题中一般不会进行删除节点,而是会开 一个数组打一个标记(如bool标记)
数学上取质数,而且举例2的幂尽可能远冲突概率最小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <cstring> #include <iostream> using namespace std;const int N = 1e5 + 3 ; int h[N], e[N], ne[N], idx; void insert (int x) { int k = (x % N + N) % N; e[idx] = x; ne[idx] = h[k]; h[k] = idx++; } bool find (int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1 ; i = ne[i]) { if (e[i] == x) { return true ; } } return false ; } int n;int main () { cin >> n; memset (h, -1 , sizeof h); while (n--) { string op; int x; cin >> op >> x; if (op == "I" ) { insert (x); } else { if (find (x)) { puts ("Yes" ); } else { puts ("No" ); } } } return 0 ; }
缺点:消耗空间
开放地址法 Open Addressing 思路:只用一个一维数组来模拟哈希表,因此形式会相对简洁,但是一般来说一维数组的长度应该是需要的数组大小的2~3倍,
类似数组模拟链表的方式,将数据存储在空余空间中,想象上厕所,一个坑位完了就下一个
探测方法:
缺点:费时间,元素容易聚集,分布不均匀,聚集越多性能越差
避免了元素的聚集,如果顺序表长度为指数,顺序表空位多于一半,平方探测总能插入新元素
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <cstring> #include <iostream> using namespace std;const int N = 2e5 + 3 ; const int null = 0x3f3f3f3f ; int h[N];int find (int x) { int t = (x % N + N) % N; while (h[t] != null && h[t] != x) { t++; if (t == N) { t = 0 ; } } return t; } int n;int main () { cin >> n; memset (h, 0x3f , sizeof h); while (n--) { string op; int x; cin >> op >> x; if (op == "I" ) { h[find (x)] = x; } else { if (h[find (x)] == null) { puts ("No" ); } else { puts ("Yes" ); } } } return 0 ; }
杜鹃哈希😎 感觉:拆东墙补西墙,但是很牛逼
优点:只要两个表中间有元素,一定可以通过哈希函数在顺序表中直接找到不用探测,非常高效
原因:所有索引位置都是哈希函数得到
失败时:
缺点:元素过多时候插入元素困难
(101条消息) 杜鹃散列_EmberWn的博客-CSDN博客_杜鹃散列
【散列】杜鹃散列详情与C++实现代码 - awst_lee - 博客园 (cnblogs.com)
Cuckoo hash算法分析 - 可酷可乐 - 博客园 (cnblogs.com)
Cuckoo hashing - Wikipedia (科学上网看)
杜鹃哈希举例:
坏消息:根本看不懂
好消息:不用看了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 template <typename AnyType>class CuckooHashFamily {public : size_t hash (const AnyType& x, int which) const ; int getNumberOfFunctions () ; void generateNewFunctions () ; }; template <int count>class StringHashFamily {private : std::vector<int > MULTIPLIERS; UniformRandom r; public : StringHashFamily () :MULTIPLIERS (count) { generateNewFuntions (); } int getNumberOfFunctions () const { return count; } void generateNewFuntions () { for (auto & mult : MULTIPLIERS) mult = r.nextInt (); } size_t hash (const string& x, int which) const { const int multiplier = MULTIPLIERS[which]; size_t hashVal = 0 ; for (auto ch : x) hashVal = multiplier * hashVal + ch; return hashVal; } }; template <typename AnyType, typename HashFamily>class HashTable {private : struct HashEntry { AnyType element; bool isActive; HashEntry (const AnyType&e=AnyType (),bool a=false ) :element{e},isActive{a}{} HashEntry (AnyType&&e,bool a=false ) :element{std::move (e)},isActive{a}{} }; bool insertHelper1 (const AnyType& xx) { const int COUNT_LIMIT = 100 ; AnyType x = xx; while (true ) { int lastPos = -1 ; int pos; for (int count = 0 ; count < COUNT_LIMIT; ++count) { for (int i = 0 ; i < numHashFunctions; ++i) pos = myhash (x, i); if (!isActive (pos)) { array[pos] = std::move (HashEntry{ std::move (x),true }); ++currentSize; return true ; } } int i = 0 ; do { pos = myhash (x, r.nextInt (numHashFunctions)); } while (pos == lastPos && i++ < 5 ); lastPos = pos; std::swap (x, array[pos].element); } if (++rehashes > ALLOWED_REHASHES) { expand (); rehashes = 0 ; } else rehash (); } bool insertHelper1 (AnyType&& x) { const int COUNT_LIMIT = 100 ; while (true ) { int lastPos = -1 ; int pos; for (int count = 0 ; count < COUNT_LIMIT; ++count) { for (int i = 0 ; i < numHashFunctions; ++i) pos = myhash (x, i); if (!isActive (pos)) { array[pos] = std::move (HashEntry{ std::move (x),true }); ++currentSize; return true ; } } int i = 0 ; do { pos = myhash (x, r.nextInt (numHashFunctions)); } while (pos == lastPos && i++ < 5 ); lastPos = pos; std::swap (x, array[pos].element); } if (++rehashes > ALLOWED_REHASHES) { expand (); rehashes = 0 ; } else rehash (); } bool isActive (int currentPos) const { return currentPos != -1 && array[currentPos].isActive; } size_t myhash (const AnyType& x, int which) const { return hashFunctions.hash (x, which) % array.size (); } int findPos (const AnyType& x) const { for (int i = 0 ; i < numHashFunctions; ++i) { int pos = myhash (x, i); if (isActive (pos) && array[pos].element == x) return pos; } return -1 ; } void expand () { rehash (static_cast <int >(array.size () / MAX_LOAD)); } void rehash () { hashFunctions.generateNewFuntions (); rehash (array.size ()); } void rehash (int newSize) { std::vector<HashEntry> oldArray = array; array.resize (nextPrime (newSize)); for (auto & entry : array) entry.isActive = false ; currentSize = 0 ; for (auto & entry : oldArray) if (entry.isActive) insert (std::move (entry.element)); } constexpr static const double MAX_LOAD=0.4 ; static const int ALLOWED_REHASHES = 5 ; vector<HashEntry>array; int currentSize; int numHashFunctions; int rehashes; UniformRandom r; HashFamily hashFunctions; public : explicit HashTable (int size = 101 ) :array(nextPrime(size)) { numHashFunctions = hashFunctions.getNumberOfFunctions (); rehashes = 0 ; makeEmpty (); } void makeEmpty () { currentSize = 0 ; for (auto & entry : array) entry.isActive = false ; } bool contains (const AnyType& x) const { return findPos (x) != -1 ; } bool remove (const AnyType& x) { int currentPos = findPos (x); if (!isActive (currentPos)) return false ; array[currentPos].isActive = false ; --currentSize; return true ; } bool insert (const AnyType& x) { if (contains (x)) return false ; if (currentSize >= array.size () * MAX_LOAD) expand (); return insertHelper1 (x); } bool insert (AnyType&& x) { if (contains (x)) return false ; if (currentSize >= array.size () * MAX_LOAD) expand (); return insertHelper1 (std::move (x)); } int size () const { return currentSize; } int capacity () const { return array.size (); } };
树 最后通过看大神的代码才恍然大悟,二叉树的建立,需要按照一棵满二叉树来建立
问题来了,我们的节点不够满二叉树的,这就是关键,空节点也需要补上!
使用数组构造一棵二叉树也是同理!
完全二叉树的构建(0作为空节点,会有空间浪费) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <iostream> using namespace std;const int M=1010 ;int tree[M]={0 };int judge=1 ;int real_depth (int n) { int ans=0 ; while (n!=0 ) { ans++; n=n/2 ; } return ans; } void DLR (int i) { if (tree[i]!=0 ) { cout<<tree[i]<<" " ; DLR (i*2 ); DLR (i*2 +1 ); } } void LDR (int i) { if (tree[i]!=0 ) { DLR (i*2 ); cout<<tree[i]<<" " ; DLR (i*2 +1 ); } } void LRD (int i) { if (tree[i]!=0 ) { DLR (i*2 ); DLR (i*2 +1 ); cout<<tree[i]<<" " ; } } int main () { int n; cin>>n; int node_num=1 ; for (int j=0 ;j<n;j++ ) { int node; cin>>node; tree[1 ]=node; int i=1 ; if (node==0 ||node==-1 )continue ; while (judge==1 ) { cin>> node; ++i; if (node==-1 ) { cout<<real_depth (i-1 ) <<" " ; break ; } else { if (tree[i/2 ]!=0 ) { tree[i]=node; } if (tree[i/2 ]==0 ) { while (tree[i/2 ]==0 )i=i/2 ; i=i/2 ; while (tree[i*2 ])i=i*2 ; i=i*2 ; tree[i]=node; } } node_num=i; } DLR (1 ); cout<<endl; for (int i=1 ;i<=node_num;i++) { tree[i]=0 ; } } }
树的恢复 (105条消息) 先序遍历中序遍历还原二叉树_May Hacker的博客-CSDN博客_先序遍历中序遍历还原树
由先序和中序恢复二叉树 理论基础:
105. 从前序与中序遍历序列构造二叉树 - 力扣(Leetcode)
背也要背会的模板: [(105条消息) 根据先序中序还原二叉树_BugMaker-shen的博客-CSDN博客_由先序和中序恢复二叉树](https://blog.csdn.net/qq_42500831/article/details/105984986?ops_request_misc=&request_id=&biz_id=102&utm_term=先序 + 中序 恢复二叉树&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-0-105984986.142^v63^control,201^v3^control_1,213^v2^t3_control1&spm=1018.2226.3001.4187)
原理:使用递归
🙏🙏🙏🙏🙏🙏🙏🙏🙏感谢这位西电的朋友助我脱离苦海,感谢感谢
可以直接用那种:😭😭😭😭(数据结构放假是机考!机考!机考!我giao!!! )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <iostream> #include <vector> #include <algorithm> using namespace std;struct Node { char data; Node* left; Node* right; Node (char data){ this ->data = data; this ->left = nullptr ; this ->right = nullptr ; } }; vector<char > getCharArray (string str) { vector<char > res; for (char c : str){ res.push_back (c); } return res; } Node* getTree (vector<char >& preOrder, vector<char >& inOrder) { if (preOrder.empty ()){ return nullptr ; } Node* root = new Node (preOrder[0 ]); vector<char >::iterator mid = find (inOrder.begin (), inOrder.end (), preOrder[0 ]); int left_nodes = mid - inOrder.begin (); vector<char > left_inOrder (inOrder.begin(), mid) ; vector<char > right_inOrder (mid+1 , inOrder.end()) ; vector<char > left_preOrder (preOrder.begin()+1 , preOrder.begin()+1 +left_nodes) ; vector<char > right_preOrder (preOrder.begin()+1 +left_nodes, preOrder.end()) ; root->left = getTree (left_preOrder, left_inOrder); root->right = getTree (right_preOrder, right_inOrder); return root; } void postOrder (Node* root) { if (root == nullptr ){ return ; } postOrder (root->left); postOrder (root->right); cout<<root->data; } int main () { string pre_str; string in_str; while (cin >> pre_str >> in_str){ vector<char > preOrder = getCharArray (pre_str); vector<char > inOrder = getCharArray (in_str); Node* root = getTree (preOrder, inOrder); postOrder (root); cout<<endl; } return 0 ; }
LeetCode解法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 class Solution {private : TreeNode* traversal (vector<int >& inorder, int inorderBegin, int inorderEnd, vector<int >& preorder, int preorderBegin, int preorderEnd) { if (preorderBegin == preorderEnd) return NULL ; int rootValue = preorder[preorderBegin]; TreeNode* root = new TreeNode (rootValue); if (preorderEnd - preorderBegin == 1 ) return root; int delimiterIndex; for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { if (inorder[delimiterIndex] == rootValue) break ; } int leftInorderBegin = inorderBegin; int leftInorderEnd = delimiterIndex; int rightInorderBegin = delimiterIndex + 1 ; int rightInorderEnd = inorderEnd; int leftPreorderBegin = preorderBegin + 1 ; int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin); int rightPreorderEnd = preorderEnd; cout << "----------" << endl; cout << "leftInorder :" ; for (int i = leftInorderBegin; i < leftInorderEnd; i++) { cout << inorder[i] << " " ; } cout << endl; cout << "rightInorder :" ; for (int i = rightInorderBegin; i < rightInorderEnd; i++) { cout << inorder[i] << " " ; } cout << endl; cout << "leftPreorder :" ; for (int i = leftPreorderBegin; i < leftPreorderEnd; i++) { cout << preorder[i] << " " ; } cout << endl; cout << "rightPreorder :" ; for (int i = rightPreorderBegin; i < rightPreorderEnd; i++) { cout << preorder[i] << " " ; } cout << endl; root->left = traversal (inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); root->right = traversal (inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); return root; } public : TreeNode* buildTree (vector<int >& preorder, vector<int >& inorder) { if (inorder.size () == 0 || preorder.size () == 0 ) return NULL ; return traversal (inorder, 0 , inorder.size (), preorder, 0 , preorder.size ()); } };
并查集——面试涉及——竞赛频率也高😍 面试官一般都习惯去问一些代码短但是思路精巧的(便于手写代码)
提前学习知识:链表,线性表
全名:归并查找集合,即判断两个元素是否属于同一个集合
836. 合并集合 - AcWing题库
可选方法:
在集合经常动态变化时候,查找方法效率并不高
并查集应用: 快速处理下列问题:
将两个集合合并
询问两个元素是否属于同一个集合中
并查集能在近乎O(1)的时间复杂度之内快速支持以上两种操作
正常:合并元素方式,至少需要对线性表/链表整个进行一次遍历,耗时高
基本原理: 每一个集合使用一棵树来维护(不一定是二叉树,可能是三叉树,B+树等),每一个集合的编号就是根节点的编号 ,树中对于每一个点都存储其父节点(用p[x]表示x的父节点),在求某个元素是否属于某一个集合的时候,就在该元素向上遍历,知道到达根节点,最后判断根节点的编号是否是所需编号
解决问题与流程:
如何判断树根,
如何求x的集合编号:
如何合并两个集合:加一条边,将一个树插入到另一棵树身上就可以
写法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int find (int x) { if (p[x]!=x)p[x]=find (p[x]); return p[x]; } void merge (int x,int y) { p[x]=y; }
全代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> using namespace std;const int N=100010 ;int p[N];int find (int x) { if (p[x]!=x) p[x]=find (p[x]); return p[x]; } void merge (int x ,int y) { p[find (x)]=find (y); } int main () { int n,m; scanf ("%d%d" ,&n,&m); for (int i=1 ;i<=n;i++) p[i]=i; while (m--) { char op[2 ]; int a,b; scanf ("%s%d%d" ,op,&a,&b); if (*op=='M' ) merge (a,b); else if (find (a)==find (b)) printf ("Yes\n" ); else printf ("No\n" ); } return 0 ; }
并查集优化:路径压缩 并查集最牛逼的优化方式,另一种优化基本写代码时候不会使用
原理:一旦在向上走的时候找到了根节点,将该路径所有节点都指向根节点 ,即只需要搜索一次,之后的重复操作速度就会提高,可以视为O(1)优化
优化:加权合并 防止树越来越高
记录每棵树元素的个数作为树的权重
堆:完全二叉树的操作 应用:
(不用stl)手搓一个堆,堆的基本
插入一个数
求这个集合当中的最小值
删除最小值
删除任意一个元素(stl无法直接实现)
修改任意一个元素(stl无法直接实现)
堆的基本结构: 堆属于一棵完全 二叉树(指除了最后一排其他都是均匀分布,即所有节点都不是非空),最后一层节点从左到右依次排列
小根堆的性质:每一个点都是小于等于左右儿子(即递归定义),则根节点就是最小值
大根堆:相反
凡是完全二叉树都是用一维数组存储的
1号点是根节点
x的左儿子下标:2x,
x的右儿子下标:2x+1
stl里面的堆就是优先队列
特点:
时间复杂度非常稳定,不依赖原始记录状态
是一种不稳定的排序方法(记录比较与交换跳跃进行)
基本函数: down(x):如果某一个点的值变大,就将该值向下压
up(x):如果一个点的值变小,向上升
两个函数的执行次数都和二叉树的深度成正比,也就是logn
这里的x实际上是所处的位置
由基本函数构成堆的几种操作:
插入一个数
1 2 heap[++size]=x; up (size);
求最小值
删除最小值(也就是最顶部的根节点的删除)
思路 :用堆底部的最后一个元素覆盖掉第一个元素,然后进行down(1)
原理:存储结构是一个一维数组 ,删除尾部节点很容易(直接size–就行),但删除头部却很麻烦
覆盖掉之后再使用向下函数down会让顶部元素下沉到正确位置
1 2 3 heap[1 ]=heap[size]; size--; down (1 );
删除任意一个元素,和删除头部不太一样在于不确定改变值之后是大还是小
简单粗暴好使的办法:管他呢,down一次,up一次,因为up和down实际上只会执行一次
1 2 3 4 heap[k]=heap[size]; size--; down (k);up (k);
修改任意一个元素的值:同删除一个元素的操作
1 2 3 heap[k]=x; down (k);up (k);
构建小细节: 不同于其它的一般采用下标从0开始,对于树形结构,因为树的性质有左儿子=根/2,如果从0开始左儿子也是0,不方便,因此堆排序实际上是从1开始
开始构建: 构建堆 可以用插入的方式操作,但每一次插入都是logn,实际上不好
有时间复杂度为on的方式
1 for (int i=size/2 ,i>0 ;i--)down (i);
构建up 妙啊(发出抱大佬大腿的声音)😍
1 2 3 4 5 6 7 8 void up (int u) { while (u / 2 && h[u] < h[u / 2 ]) { swap (u, u / 2 ); u >>= 1 ; } }
构建down 1 2 3 4 5 6 7 8 9 10 11 12 13 void down (int u) { int t=u; if (u*2 <=size&&h[u*2 ]<h[t])t=u*2 ; if (u*2 +1 <=size&&h[u*2 +1 ]<=h[t])t=u*2 +1 ; if (u!=t) { swap (h[u],h[t]); down (t); } }
模拟堆 难点:支持随机的修改和删除 ,题中要求是第i个,但是i对应的下标会随着操作变化下标跟着变化,需要实时更新。因此映射也要交换好
解法:使用映射,老牛逼了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int h[N], ph[N], hp[N], size;void heap_swap (int a, int b) { swap (ph[hp[a]],ph[hp[b]]); swap (hp[a], hp[b]); swap (h[a], h[b]); } void down (int u) { int t = u; if (u * 2 <= size && h[u * 2 ] < h[t]) t = u * 2 ; if (u * 2 + 1 <= size && h[u * 2 + 1 ] < h[t]) t = u * 2 + 1 ; if (u != t) { heap_swap (u, t); down (t); } } void up (int u) { while (u / 2 && h[u] < h[u / 2 ]) { heap_swap (u, u / 2 ); u >>= 1 ; } } for (int i = n / 2 ; i; i -- ) down (i);
解题技巧: 当题中有明确给出第i个操作数,要考虑如何通过更新操作数的下标
堆排序:onlogn 一般升序使用大顶堆,降序采用小顶堆
举例:对数组进行从小到大排序,输出前m小的数
思路:本题可以使用堆排序,构造小顶堆,然后输出堆顶,输出后把堆顶和堆尾交换。尾部边界缩小,重复执行m次即可。
注意点:
代码: 易错点:
模拟堆(直接造小根堆) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> using namespace std;const int M=100100 ;int h[M];int size;void big_down (int u) { int t=u; if (u*2 <=size&&h[u*2 ]<h[t])t=u*2 ; if (u*2 +1 <=size&&h[u*2 +1 ]<=h[t])t=u*2 +1 ; if (u!=t) { swap (h[u],h[t]); big_down (t); } } void small_down (int u) { int t=u; if (u*2 <=size&&h[u*2 ]>h[t])t=u*2 ; if (u*2 +1 <=size&&h[u*2 +1 ]>=h[t])t=u*2 +1 ; if (u!=t) { swap (h[u],h[t]); small_down (t); 注意这里向下的还是t } } int main () { cin>>size; for (int i=1 ;i<=size;i++) { cin>>h[i]; } for (int i = size / 2 ; i; i -- ) small_down (i); for (int i=1 ;i<=size;i++)cout<<h[i]<<' ' ; cout<<endl; for (int i = size / 2 ; i; i -- ) big_down (i); for (int i=1 ;i<=size;i++)cout<<h[i]<<' ' ; cout<<endl; return 0 ; }
堆排序(整体) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <iostream> #include <cstring> #include <algorithm> using namespace std;const int N = 100010 ;int a[N];int n, m;int r ;void down (int u) { int t = u; 易错部分:这里前面都是u最后对比是t,修改大小也是修改u if (2 * u <= r && a[2 * u] < a[t]) t = 2 * u; if (2 * u + 1 <= r && a[2 * u + 1 ] < a[t]) t = 2 * u + 1 ; if (u != t) { swap (a[u], a[t]); down (t); } } 因为堆是一棵二叉树构建起来的,因此是从1 int main () { cin >> n >> m; r = n; for (int i = 1 ; i <= n; i ++ ) { int x; cin >> a[i]; } for (int i = n /2 ; i ; i--) { down (i); } while (m -- ) { cout << a[1 ] << " " ; swap (a[1 ], a[r]); r--; down (1 ); } }
哈夫曼树 下面这个关于编码的,很重要
(108条消息) 【数据结构——哈夫曼树及其应用】_FEI..的博客-CSDN博客_哈夫曼树的parent怎么求
哈夫曼编码:字母 核心:无前缀编码 ,从根节点到叶子节点的路径代表编码,只要字母在叶子节点,对应编码就是无前缀
贪心算法
自己写的代码(输入字符串进行编码并输出): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 #include <iostream> #include <string> #include <cstring> using namespace std;typedef struct { int weight; int parent, lchild, rchild; }HTnode,*HuffmanTree; void Select (HuffmanTree& HT,int &s1,int &s2,int n) { s2=1 ,s1=2 ; for (int i = 1 ;i <= n;++s1) { if (HT[s1].parent==0 &&HT[s2].parent==0 ) { break ; } if (HT[s1].parent==0 ) { s2=s1; } if (s2==s1)++s1; } for (int i = 1 ;i <= n;++i) { if (HT[i].weight<=HT[s1].weight&&HT[i].parent==0 ) { s1=i; } } for (int i = 1 ;i <= n;++i) { if (HT[i].weight<=HT[s2].weight&&HT[i].parent==0 &&i!=s1) { s2=i; } } } void CreateHuffmanTree (HuffmanTree& HT, int n,int word[]) { int s1,s2; if (n <= 1 ) return ; int m = 2 * n - 1 ; HT = new HTnode[m + 1 ]; for (int i = 1 ;i <= m;++i) { HT[i].parent = 0 ; HT[i].lchild = 0 ; HT[i].rchild = 0 ; } int word_select=0 ; for (int i = 1 ;i <= n;++i) { for (;word[word_select]==0 ;word_select++); HT[i].weight=word[word_select]; word_select++; } for (int i = n + 1 ;i <= m;++i) { Select (HT, s1, s2,n); HT[s1].parent = i;HT[s2].parent = i; HT[i].lchild = s1;HT[i].rchild = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } } typedef char ** HuffmanCode;void CreatHuffmanCode (HuffmanTree HT, HuffmanCode& HC, int n) { HC = new char * [n + 1 ]; char *cd = new char [n]; int start; int c; int f; cd[n - 1 ] = '\0' ; for (int i = 1 ;i <= n;++i) { start = n - 1 ; c = i;f = HT[i].parent; while (f != 0 ) { --start; if (HT[f].lchild == c) cd[start] = '0' ; else cd[start] = '1' ; c = f;f = HT[f].parent; } HC[i] = new char [n - start]; strcpy (HC[i], &cd[start]); } delete cd; } char out[128 ]={'\0' };int word[128 ]={0 };int main () { string ans="" ; string temp; while (getline (cin, temp)) { if (temp == "0" ) { break ; } ans+=temp; } int length=ans.size (); for (int i=0 ;i<length;i++) { int idx=(int )(ans[i]); word[idx]++; } int node_num=0 ; for (int i=1 ;i<=128 ;i++) { if (word[i]!=0 ) { ++node_num; out[node_num]=(char )i; } } HuffmanTree HT; CreateHuffmanTree (HT,node_num,word); HuffmanCode HC; CreatHuffmanCode (HT,HC,node_num); for (int i=1 ;i<=node_num;i++) { cout<<out[i]<<":" <<HC[i]<<endl; } return 0 ; }
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 #include <iostream> #include <cstdlib> using namespace std;#define MAX_TREE_HT 100 struct MinHeapNode { char data; unsigned freq; struct MinHeapNode *left, *right; }; struct MinHeap { unsigned size; unsigned capacity; struct MinHeapNode ** array; }; struct MinHeapNode * newNode (char data, unsigned freq){ struct MinHeapNode * temp = (struct MinHeapNode*)malloc (sizeof (struct MinHeapNode)); temp->left = temp->right = NULL ; temp->data = data; temp->freq = freq; return temp; } struct MinHeap * createMinHeap (unsigned capacity){ struct MinHeap * minHeap = (struct MinHeap*)malloc (sizeof (struct MinHeap)); minHeap->size = 0 ; minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc (minHeap-> capacity * sizeof (struct MinHeapNode*)); return minHeap; } void swapMinHeapNode (struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode * t = *a; *a = *b; *b = t; } void minHeapify (struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1 ; int right = 2 * idx + 2 ; if (left < minHeap->size && minHeap->array[left]-> freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]-> freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode (&minHeap->array[smallest], &minHeap->array[idx]); minHeapify (minHeap, smallest); } } int isSizeOne (struct MinHeap* minHeap) { return (minHeap->size == 1 ); } struct MinHeapNode * extractMin (struct MinHeap* minHeap){ struct MinHeapNode * temp = minHeap->array[0 ]; minHeap->array[0 ] = minHeap->array[minHeap->size - 1 ]; --minHeap->size; minHeapify (minHeap, 0 ); return temp; } void insertMinHeap (struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1 ; while (i && minHeapNode->freq < minHeap->array[(i - 1 ) / 2 ]->freq) { minHeap->array[i] = minHeap->array[(i - 1 ) / 2 ]; i = (i - 1 ) / 2 ; } minHeap->array[i] = minHeapNode; } void buildMinHeap (struct MinHeap* minHeap) { int n = minHeap->size - 1 ; int i; for (i = (n - 1 ) / 2 ; i >= 0 ; --i) minHeapify (minHeap, i); } void printArr (int arr[], int n) { int i; for (i = 0 ; i < n; ++i) cout<< arr[i]; cout<<"\n" ; } int isLeaf (struct MinHeapNode* root) { return !(root->left) && !(root->right); } struct MinHeap * createAndBuildMinHeap (char data[], int freq[], int size){ struct MinHeap * minHeap = createMinHeap (size); for (int i = 0 ; i < size; ++i) minHeap->array[i] = newNode (data[i], freq[i]); minHeap->size = size; buildMinHeap (minHeap); return minHeap; } struct MinHeapNode * buildHuffmanTree (char data[], int freq[], int size){ struct MinHeapNode *left, *right, *top; struct MinHeap * minHeap = createAndBuildMinHeap (data, freq, size); while (!isSizeOne (minHeap)) { left = extractMin (minHeap); right = extractMin (minHeap); top = newNode ('$' , left->freq + right->freq); top->left = left; top->right = right; insertMinHeap (minHeap, top); } return extractMin (minHeap); } void printCodes (struct MinHeapNode* root, int arr[], int top) { if (root->left) { arr[top] = 0 ; printCodes (root->left, arr, top + 1 ); } if (root->right) { arr[top] = 1 ; printCodes (root->right, arr, top + 1 ); } if (isLeaf (root)) { cout<< root->data <<": " ; printArr (arr, top); } } void HuffmanCodes (char data[], int freq[], int size) { struct MinHeapNode * root = buildHuffmanTree (data, freq, size); int arr[MAX_TREE_HT], top = 0 ; printCodes (root, arr, top); } int main () { char arr[] = { 'a' , 'b' , 'c' , 'd' , 'e' , 'f' }; int freq[] = { 5 , 9 , 12 , 13 , 16 , 45 }; int size = sizeof (arr) / sizeof (arr[0 ]); HuffmanCodes (arr, freq, size); return 0 ; }
AVL树 平衡二叉树:
(110条消息) AVL树的详细实现(C++)_code_peak的博客-CSDN博客_c++实现avl树
avl树的各种延申应用:伸展树,B树,字典树