剑指offer算法题(21-40)
把奇数放在偶数的前面-21
问题
输入:一个数组,乱序,有奇数和偶数
输出:把所有的奇数放在前面,所有的偶数放在后面
暴力思路
从头到尾遍历数组
遇到奇数,访问下一个
遇到偶数,把它后面的数字都向前挪动以为,该偶数放到末尾
冒泡思路
for (int i = n-1; i > 0; i--)
依次放置好后面n-1个数即可
从for (int j = 0; j < i; j++)
,遇到j-1是偶数,j是奇数,则交换
辅助数组
新数组存偶数,原数组删除偶数,最后把新数组的偶数追加到原数组
遍历原数组,遇到偶数,存到新数组,删除原数组中的偶数
双指针快排思路
类似于快速排序 的思路,但不是稳定 的。
指针1,在前面,向后移动,前面应是奇数
指针2,在后面,向前移动,后面应是偶数
指针1指偶数,指针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 void reorder_array (vector <int > & a, bool (*f)(int )) { int l = 0 ; int r = a.size() - 1 ; while (l < r) { while (l < r && !f(a[l])) { l++; } while (r > l && f(a[r])) { r--; } if (l < r) { int t = a[l]; a[l] = a[r]; a[r] = t; } } }
归并排序思路
归并排序稳定,也很快,所以使用归并排序 。 分成长度为1、2、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 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 class Solution {public : int get_parity (int n) { return (n & 1 ) == 1 ; } void merge (vector <int > &a, int start, int mid, int end) { int *t = new int [end - start + 1 ]; int i = start; int j = mid + 1 ; int k = 0 ; while (i <= mid && j <= end) { int pi = get_parity(a[i]); int pj = get_parity(a[j]); if (pi == 1 && pj == 0 ) { t[k++] = a[i++]; } else if (pi == 0 && pj == 1 ){ t[k++] = a[j++]; } else if (pi == 1 && pj == 1 ) { t[k++] = a[i++]; } else if (pi == 0 && pj == 0 ) { t[k++] = a[i++]; } } while (i <= mid) { t[k++] = a[i++]; } while (j <= end) { t[k++] = a[j++]; } for (i = 0 ; i < end - start + 1 ; i++) { a[start + i] = t[i]; } delete [] t; return ; } void merge_groups (vector <int > &a, int gap) { int twolen = 2 * gap; int i; for (i = 0 ; i + twolen - 1 < a.size(); i += twolen) { int start = i; int mid = start + gap - 1 ; int end = i + twolen - 1 ; merge(a, start, mid, end); } if (i + gap - 1 < a.size() - 1 ) { merge(a, i, i + gap - 1 , a.size() - 1 ); } } void reOrderArray (vector <int > &a) { for (int i = 1 ; i < a.size(); i *= 2 ) { merge_groups(a, i); } } };
链表中倒数第k个节点-22
不知道链表长度,要求在\(O(n)\) 内找到倒数第k个节点,注意代码的鲁棒性。
双指针思路
两个指针,l
先走k-1步,r
再从头节点出发,始终保持距离为k-1 。 r走到末尾 时,l就是倒数第k个节点 。
注意头结点为空和k非法(为0、k超出等)的情况。
双指针求链表中间节点
两个指针,l
走一步 ,r
走两步 。r走到末尾 的时候,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 ListNode* kth_from_end (ListNode* phead, unsigned int k) { if (phead == nullptr || k == 0 ) { return nullptr ; } ListNode* pr = phead; ListNode* pl = phead; unsigned int count = 1 ; while (pl && count <= k - 1 ) { pl = pl->next; count++; } if (pl == nullptr ) { return nullptr ; } while (pl->next) { pl = pl->next; pr = pr->next; } return pr; }
链表中环的入口节点-23
如果一个链表有环,则返回这个环的入口节点
双指针法
确定有环
双指针,同时走,l一次1步,r一次2步。
r走到末尾,则无环
r与l相遇,则有环。相遇节点在环内
确定环内节点数量
相遇节点在环内,从相遇节点开始遍历一圈,计数。再次回到相遇节点,就能知道环内节点数量k 。
找到环的入口节点
双指针,r先走k步,l再走。l与r相遇时,就是环的入口节点
关键代码
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 ListNode* meet_node (ListNode* phead) { if (phead == nullptr || phead->next == nullptr ) { return nullptr ; } ListNode* pl = phead; ListNode* pr = phead->next->next; ListNode* pmeet = nullptr ; while (pl && pr) { if (pl == pr) { pmeet = pl; break ; } pl = pl->next; pr = pr->next; if (pr) { pr = pr->next; } } return pmeet; } int get_circle_node_count (ListNode* pmeet) { if (pmeet == nullptr || pmeet->next == nullptr ) { return 0 ; } ListNode* p = pmeet->next; int count = 1 ; while (p != pmeet) { p = p->next; count++; } return count; } ListNode* circle_entry_node (ListNode* phead) { ListNode* pmeet = meet_node(phead); if (pmeet == nullptr ) { return nullptr ; } int k = get_circle_node_count(pmeet); ListNode* pl = phead; ListNode* pr = phead; for (int i = 1 ; i <= k; i++) { pr = pr->next; } ListNode* pentry = nullptr ; while (pl && pr) { if (pl == pr) { pentry = pl; break ; } pl = pl->next; pr = pr->next; } return pentry; }
推导法和断链法
leetcode推导法
翻转链表-24
Reverse Linked List
思路
每次遍历保存三个节点:pre
, pnow
, pnext
,遍历到pnow
保存pnow的next
pnow指向pre
pre = pnow
把pnext赋值给pnow,下一次循环
注意当pnext为空时,已走到末尾,此时的pnow应该是新的head。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ListNode* reverseList (ListNode* head) { ListNode* pre = nullptr ; ListNode* pnow = head; ListNode* pnext = nullptr ; while (pnow) { pnext = pnow->next; pnow->next = pre; if (pnext == nullptr ) { head = pnow; } pre = pnow; pnow = pnext; } return head; }
合并两个有序链表-25
见这里
树的子结构-26
Subtree of Another Tree
两棵树s和t,判断t是否是s的一个子结构
思路
判断两颗树相同
两个都为空,相同
其中一个为空,不同
值不同,不同
则 return 左子树相同 && 右子树相同
关键代码
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 bool is_same (TreeNode* p1, TreeNode* p2) { if (p1 == nullptr && p2 == nullptr ) { return true ; } if (p1 == nullptr || p2 == nullptr ) { return false ; } if (p1->val != p2->val) { return false ; } return is_same(p1->left, p2->left) && is_same(p1->right, p2->right); } bool isSubtree (TreeNode* s, TreeNode* t) { queue <TreeNode*> nodes; if (s != nullptr ) { nodes.push(s); } while (!nodes.empty()) { TreeNode* p = nodes.front(); nodes.pop(); if (is_same(p, t)) { return true ; } if (p->left) { nodes.push(p->left); } if (p->right) { nodes.push(p->right); } } return false ; }
二叉树的镜像-27
二叉树的镜像,就是左右孩子交换节点嘛。
思路
使用先序遍历 的思想,这里是二叉树各种遍历
使用栈,根节点入栈
栈不为空,出栈一个元素p
交换其左右孩子节点
右孩子入栈,左孩子入栈
[关键代码]
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 mirror (TreeNode *head) { TreeNode* p = head; stack <TreeNode*> st; if (p != nullptr ) { st.push(p); } while (!st.empty()) { TreeNode* now = st.top(); st.pop(); TreeNode* t = now->left; now->left = now->right; now->right = t; if (now->right) { st.push(now->right); } if (now->left) { st.push(now->left); } } }
对称的二叉树-28
leetcode对称的二叉树
判断一个二叉树是不是对称的
思路
普通的遍历都是先左后右 , 对称的话,得用先右后左 。
一棵树对称 ,则先左后右的先序序列、先右后左的先序序列 应该一样。 即左右子树对称相等 。
遍历的时候
根节点为空,对称
否则,看sym(root.left, root.right)
两个节点其中一个为空,则看p1 == p2
两个都不为空,则先看根节点的值
最后则交叉看左右子树 ,sym(p1.left, p2.right) && sym(p1.right, p2.left)
[关键代码]
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 bool isSymmetric (TreeNode* root) { if (root == nullptr ) { return true ; } return symmetric_equal(root->left, root->right); } bool symmetric_equal (TreeNode* p1, TreeNode* p2) { if (p1 == nullptr || p2 == nullptr ) { return p1 == p2; } if (p1->val != p2->val) { return false ; } return symmetric_equal(p1->left, p2->right) && symmetric_equal(p1->right, p2->left); }
非递归代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public boolean isSymmetric (TreeNode root) { if (root == null ) return true ; Stack<TreeNode> stack = new Stack<>(); stack.push(root.left); stack.push(root.right); while (!stack.empty()) { TreeNode n1 = stack.pop(), n2 = stack.pop(); if (n1 == null && n2 == null ) continue ; if (n1 == null || n2 == null || n1.val != n2.val) return false ; stack.push(n1.left); stack.push(n2.right); stack.push(n1.right); stack.push(n2.left); } return true ; }
顺时针打印矩阵-29
牛客网打印矩阵
思路
用左上 和右下 的坐标定位出一圈要打印的数据。一圈打完以后,分别沿着对角线向中间靠拢一个单位。
关键代码
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 vector <int > print_marix(vector <vector <int >> matrix) { vector <int > res; if (matrix.empty() || matrix[0 ].empty()) { return res; } int row = matrix.size(); int col = matrix[0 ].size(); int left = 0 , top = 0 , right = col - 1 , bottom = row - 1 ; while (left <= right && top <= bottom) { for (int i = left; i <= right; i++) res.push_back(matrix[top][i]); for (int i = top + 1 ; i <= bottom; i++) res.push_back(matrix[i][right]); if (top != bottom) for (int i = right - 1 ; i >= left; i--) res.push_back(matrix[bottom][i]); if (left != right) for (int i = bottom - 1 ; i > top; i--) res.push_back(matrix[i][left]); left++, top++, right--, bottom--; } return res; }
包含min函数的栈-30
Min Stack
实现能够得到栈中最小元素的数据结构,要求入栈、出栈、获得最小元素都是O(1)
思考过程
定义一个变量,去存储最小元素,每次对其更新。可是,当最小元素出栈以后呢,怎么去得到新的最小元素呢?这样就需要把次最小元素也存储下来。就需要把每次最小的元素,按顺序存储下来 , 按照入栈的顺序
入栈时,入栈当前最小的元素
出栈时,把当前最小的元素也出栈,留下的是下一个状态的最小元素
思路
数据栈,正常存数据
最小栈 ,存放各个时刻的最小元素,栈顶一直是当前的最小元素
入栈: 当前最小元素:min(min_st.top(), new)
,最小栈压入当前的最小元素
出栈:数据栈元素出栈,同时最小栈出掉当前的最小元素
[关键代码]
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 class MinStack {private : stack <int > st_data; stack <int > st_min; public : MinStack() { } void push (int x) { st_data.push(x); if (st_min.empty() || x < st_min.top()) { st_min.push(x); } else { st_min.push(st_min.top()); } } void pop () { if (st_data.empty() || st_min.empty()) { return ; } st_data.pop(); st_min.pop(); } int top () { return st_data.top(); } int getMin () { return st_min.top(); } };
栈的压入和弹出序列-31
输入两个序列,第一个为栈的入栈序列,判断第二个是否为其出栈序列。入栈所有数字都不相等
思路
入栈序列,出栈序列。当前需要出栈元素为i
i在栈内 ,则直接出栈
i不在栈内 ,则把如栈序列 前面到i 的元素全部入栈,再重复1
入栈序列全都入栈了,依然没有i ,则不是弹出序列
示例
入栈:1 2 3 4 5
, 出栈 4 5 3 2 1
4不在栈顶,前面元素入栈
4不在栈顶,4入栈
1 2 3 4
4 , 5 3 2 1
5
4出栈
1 2 3
5 3 2 1
5
5不在栈顶,5入栈
1 2 3 5
5, 3 2 1
-
5出栈
1 2 3
3 2 1
-
3出栈
1 2
2 1
-
2出栈
1
1
-
1出栈
-
-
-
入栈:1 2 3 4 5
, 出栈 4 3 5 1 2
4不在栈顶,4入栈
1 2 3 4
4 , 3 5 1 2
5
4出栈
1 2 3
3 5 1 2
5
3出栈
1 2
5 1 2
5
5出栈,不在栈顶, 入栈
1 2 5
5, 1 2
5出栈
1 2
1 2
1出栈,不在栈顶 ,已经无可入元素 , 终止
[关键代码]
遍历每个出栈元素now
使其在栈顶,对如栈序列进行入栈,直到now
如果now依然不在栈顶,则不是
如果now在栈顶,则出栈
继续遍历下一个出栈now
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 bool is_poporder (const vector <int >& vpush, const vector <int >& vpop) { bool res = false ; stack <int > st; int k = 0 ; for (int i = 0 ; i < vpop.size(); i++) { int now = vpop[i]; if (st.empty() || st.top() != now) { while (k < vpush.size()) { st.push(vpush[k]); if (vpush[k] == now) { k++; break ; } k++; } } if (st.empty() || now != st.top()) { res =false ; break ; } st.pop(); if (i == vpop.size() - 1 ) { res = true ; } } return res; }
从上到下打印二叉树-32
leetcode层次遍历
有3个题
层次遍历序列
每次遍历一层
分行层次遍历打印
每次打印一层
z型遍历二叉树
见leetcode的z型遍历二叉树
二叉搜索树的后序遍历-33
给一个数组,判断是否是二叉搜索树的后序遍历
后序遍历:最后一个是根节点
BST:左 < 根 < 右
给一个数组:5 7 6 9 11 10 8
根节点是8,
5 7 6
前面小于8的,是左子树,全部小于 8
9 11 10
中间大于8的,是右子树,全部大于 8
再依次取判断左右子树是否是BST
思路
判断nums[start, end]
是否是BST的后序遍历序列
空返回false,一个元素返回true。
找到根节点root = nums[end]
从start
开始,找左子树 的最后一个元素i-1
, 直到end-1
, 每个元素都小于根节点
从i
开始, 找右子树 ,直到end-1
, 每个元素都要大于根节点 。
右子树时,前面左子树已经ok(有或者没有),所以后面的元素都是右子树的 ,都要大于根节点 。
如果后面有小于根节点的,则不满足右子树,返回false
递归判断左右子树是否是BST ,返回结果。
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 bool is_bst_postorder (const vector <int > &nums, int start, int end) { if (nums.empty() || start < 0 || end > nums.size() - 1 || start > end) { return false ; } if (start == end) { return true ; } int root = nums[end]; int i = start; for (; i <= end - 1 ; i++) { if (nums[i] > root) break ; } int j = i; for (; j <= end - 1 ; j++) { if (nums[i] < root) { return false ; } } bool left = true ; if (i > start) { left = is_bst_postorder(nums, start, i - 1 ); } bool right = true ; if (j > i) { right = is_bst_postorder(nums, i, j - 1 ); } return left && right; }
二叉树中的路径求和-34
参考leetcode中各种pathsum汇总
复杂链表的复制-35
链表:值,下一个节点,任意一个节点。复制这样的一个链表。
思路1 暴力解法
先把链表连接起来,不管任意指针
遍历链表,为每个新节点找到任意指针 的节点,连接起来
同时知道旧节点N、新节点N1 、旧节点的任意指针节点S
遍历新链表,找到任意指针S1
把N1和S1连接起来
时间复杂度\(O(n^2)\) ,遍历新链表,找到任意指针S1的节点。
思路2 Hash解法
上面耗时间:找任意指针S1 。用HashMap 建立(N, N1)
的配对,就能够\(O(1)\) 查找到S1 。从而总时间为\(O(n)\)
思路3 最优-新旧链表先连再断
连接新节点 。把N1连接到N后面,a-a1-b-b1-c-c1
设置随机节点 。设置N1的S1,实际上,a-s
, a-a1
,s-s1
,所以能够找到a1-s1
新旧节点断开 。把N和N1断开,得到a1-b1-c1
注意:设置随机节点时,随机节点可能为空。断链时,下一个节点可能为空。
[关键代码]
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 RandomListNode* clone (RandomListNode* head) { if (head == nullptr ) { return nullptr ; } RandomListNode* p = head; while (p) { RandomListNode* p1 = new RandomListNode(p->label); p1->next = p->next; p->next = p1; p = p1->next; } p = head; while (p) { RandomListNode* p1 = p->next; RandomListNode* s = p->random; if (s != nullptr ) { p1->random = s->next; } p = p1->next; } p = head; RandomListNode* head1 = p->next; while (p) { RandomListNode* p1 = p->next; p->next = p1->next; if (p->next) { p1->next = p->next->next; } else { p1->next = nullptr ; } p = p->next; } return head1; }
二叉搜索树转双向链表-36
给一个二叉搜索树,转化为一个排好序的双向链表。左孩子-前一个节点,右孩子-后一个节点。
牛客网二叉搜索树与双向链表 , 类似题型:有序链表转平衡BST
BST的中序遍历 就是自动有序的。
递归思路
中序遍历,使用pre
来记录链表中的最后一个元素。
遍历到根节点时,递归转换左子树
pre与root连接,pre.right=root
, root.left=pre
, pre=root
。注意pre为空时,pre=root
递归创建右子树
再从pre找到第一个节点。
[关键代码]
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 TreeNode* convert (TreeNode* root) { if (root == nullptr ) { return nullptr ; } TreeNode* pre = nullptr ; convert_inorder(root, pre); while (pre && pre->left) { pre = pre->left; } return pre; } void convert_inorder (TreeNode* root, TreeNode* &pre) { if (root == nullptr ) { return ; } convert_inorder(root->left, pre); if (pre != nullptr ) { root->left = pre; pre->right = root; pre = root; } else { pre = root; } convert_inorder(root->right, pre); }
非递归思路
中序遍历时,记录pre
节点,每次进行修改即可,访问到p时,则连接到pre即可。
p不为空,p入栈,p=p.left
, 扫描左孩子
p为空,p从栈顶获得,p=st.top()
, 显然p没有左孩子或者左孩子已经出栈遍历过,p访问出栈 。此时,把p与pre连接即可 , 注意pre为空
扫描右孩子 ,p = p.right
最后从末尾,找到头结点
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 TreeNode* convert_stack (TreeNode* root) { if (root == nullptr ) { return nullptr ; } stack <TreeNode*> st; TreeNode* p = root; TreeNode* pre = nullptr ; while (p || !st.empty()) { if (p) { st.push(p); p = p->left; } else { p = st.top(); st.pop(); if (pre == nullptr ) { pre = p; } else { pre->right = p; p->left = pre; pre = p; } p = p->right; } } while (pre && pre->left) { pre = pre->left; } return pre; }
序列化和反序列化二叉树-37
leetcode序列化和反序列化二叉树笔记
数字全排列-38
leetcode数字全排列笔记
数组中出现次数超过一半的数-39
Leetcode笔记
最小的k个数-40
牛客最小的k个数
给一个数组,找到最小的k个数
思路1 快排思路
修改原数组。partition左边即可。O(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 vector <int > get_leastnums_by_partition(vector <int >& a, int k) { if (a.empty() || k < 1 || k > a.size()) { return vector <int >(); } int l = 0 , r = a.size() - 1 ; int i = partition(a, l, r); while (i + 1 != k) { if (i + 1 > k) { r = i - 1 ; } else if (i + 1 < k) { l = i + 1 ; } i = partition(a, l, r); } vector <int > res(k); std ::copy(a.begin(), a.begin() + k, res.begin()); return res; } int partition (vector <int >& a, int l, int r) { int x = a[l]; while (l < r) { while (l < r && a[r] >= x) { r--; } if (l < r) { a[l++] = a[r]; } while (l < r && a[l] <= x) { l++; } if (l < r) { a[r--] = a[l]; } } a[l] = x; return l; }
思路2 最大堆
不修改原数组,用最大堆找到最小的k个数。O(nlogk)
。自己的堆操作 和stl堆操作 。 关键代码 如下:
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 vector <int > get_leastknums_by_heap(vector <int >& a, int k) { if (a.empty() || k <= 0 || k > a.size()) { return vector <int >(); } vector <int > res(k); std ::copy(a.begin(), a.begin() + k, res.begin()); std ::make_heap(res.begin(), res.end()); for (auto it = a.begin() + k; it != a.end(); it++) { auto n = *it; printf ("n=%d, max=%d\n" , n, res[0 ]); if (n >= res[0 ]) { continue ; } std ::pop_heap(res.begin(), res.end()); res[k - 1 ] = n; std ::push_heap(res.begin(), res.end()); } return res; }
数据流中的中位数-41
给一个数据流,找到数组排序之后的中间的数值。奇数,中间;偶数,中间两个数的平均值。
牛客网数据流中的中位数
未排序数组
O(1)
O(n)
快排partition方法
排序数组
O(n)
O(1)
排序链表
O(n)
O(1)
两个指针指向中间节点
二叉搜索树
平均O(logn),最差O(n)
平均O(logn),最差O(n)
AVL树
O(logn)
O(1)
最大堆和最小堆
O(logn)
O(1)
堆思路
两个容器(最大堆和最小堆),数据量平均分配 ,左边全部小于右边 ,左右两边无需排好序, 根据左边最大的 ,右边最小的 来找到中位数
。
总体思路
左边小,右边大。左边最大堆max,右边最小堆min。
先放右边,后方左边。
奇数:右边min[0]
; 偶数:求平均(max[0] + min[0]) / 2
插入右边
索引为偶数时,放右边。从0开始
num较大,直接插入右边min堆
num较小,插入左边max堆,再把max插入右边min堆。num < 左边max[0]
插入左边
索引为奇数时,放左边min堆。
num较小,直接插入左边min堆
num较大,插入右边min堆,再把min插入右边max堆。num > 右边min[0]
Githubd代码
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 class Solution {private : vector <int > max; vector <int > min; public : void insert2min (int num) { if (max.size() > 0 && num < max[0 ]) { max.push_back(num); std ::push_heap(max.begin(), max.end(), less<int >()); std ::pop_heap(max.begin(), max.end(), less<int >()); num = max.back(); max.pop_back(); } min.push_back(num); std ::push_heap(min.begin(), min.end(), greater<int >()); } void insert2max (int num) { if (min.size() > 0 && num > min[0 ]) { min.push_back(num); std ::push_heap(min.begin(), min.end(), greater<int >()); std ::pop_heap(min.begin(), min.end(), greater<int >()); num = min.back(); min.pop_back(); } max.push_back(num); std ::push_heap(max.begin(), max.end(), less<int >()); } void Insert (int num) { int idx = max.size() + min.size(); if ((idx & 1 ) == 1 ) { insert2max(num); } else { insert2min(num); } } double GetMedian () { int size = min.size() + max.size(); if (size == 0 ) { return -1 ; } else if (size & 1 ) { return min[0 ]; } else { return (min[0 ] + max[0 ]) / 2.0 ; } } };
连续子数组的最大和-42
给一个数组,有正数有负数。求所有连续子数组和的最大值。O(n)
牛客网连续子数组的最大和 ,github代码
思路1 累加切换思路
当前累加和,最大累加和。遍历遇到数n
如果cursum <= 0
, 则cursum = n
, 否则 cursum += n
如果cursum > bestsum
, 则bestsum = cursum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int maxsum_subarray (const vector <int >& a) { if (a.empty()) { return 0 ; } int cursum = a[0 ]; int bestsum = a[0 ]; for (int i = 1 ; i < a.size(); i++) { if (cursum <= 0 ) { cursum = a[i]; } else { cursum += a[i]; } if (cursum > bestsum) { bestsum = cursum; } } return bestsum; }
思路2 动态规划
设f[i]
表示以\(a_i\) 结尾的子数组的最大和,我们要求max[fi]
\[
f(i) =
\begin{cases}
& a_i, & f(i-1) \le 0 \quad or \quad i = 0\\
& f(i-1) + a_i, & f(i-1) > 0 \quad and \quad i \ne 0 \\
\end{cases}
\]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int maxsum_subarray_dp (const vector <int >& a) { if (a.empty()) { return 0 ; } vector <int > p(a.size(), 0 ); for (int i = 0 ; i < a.size(); i++) { if (i == 0 || p[i-1 ] <= 0 ) { p[i] = a[i]; } else if (p[i-1 ] > 0 ) { p[i] = p[i-1 ] + a[i]; } } return *std ::max_element(p.begin(), p.end()); }
1-n整数中1出现的次数-43
输入一个n,求问1-n数字中,1出现的总个数。
牛客网1-n整数中1出现的次数 ,github源码
思路1 暴力无offer思路
用个count计数,遍历所有的数字,求每一个数字的1的个数,再求和。O(nlogn)
1 2 3 4 5 6 7 8 9 10 11 12 13 int numberof1 (unsigned int n) { int count = 0 ; while (n) { if (n % 10 == 1 ) { count++; } n = n / 10 ; } return count; }
思路2 分治递归思路
例子,找到1-21345中1的个数。分为两段:1346-21345
(当前解),1-1345
(递归解)
思路总结
21345,先求位数5和最高位2,
1346-21345,计算最高位为1 的数量h1
。h >= 2
时, \(10^4\) 。 h==1
时, 做减法。
1346-21345,计算其余位为1 的数量o1
。选1位为1,剩余位0-9任选,排列组合 。\(4\cdot10^3 \cdot 2\)
递归计算 1-1345中1的个数r1
返回h1+o1+r1
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 int numberof1_between1andn (int n) { if (n <= 9 ) { return n >= 1 ? 1 : 0 ; } string ns = std ::to_string(n); int len = ns.size(); int h = ns[0 ] - '0' ; int h1 = 0 ; if (h >= 2 ) { h1 = std ::pow (10 , len-1 ); } else { h1 = n - pow (10 , len-1 ) + 1 ; } int o1 = h * (len-1 ) * pow (10 , len-2 ); int p = h * pow (10 , len-1 ); int r = n - h * pow (10 , len-1 ); int r1 = numberof1_between1andn(r); return h1 + o1 + r1; }
数字序列中某一位的数字-44
数字序列:01234567891011121314.... 给位数,返回该位置上的数字。
例子 , 第1001位
1位数,10个,不在此,找后面的1001-10=991位
2位数,180个,不在此,找后面的991-180=881位
3位数,2700个,在此。881/3=270,881%3=1。即,从100开始的第270个数中的第2位。即370中的第2位7
github代码
注意每次递减index时,减的是位数*总数量,即index -= numcount * digits
在某个d位数中时,先算出d位数,再算出在d位数字内的res_index
,反正是除法求余 。
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 int digit_in_sequence (int index) { if (index < 0 ) { return -1 ; } int res = 0 ; int digits = 1 ; while (true ) { int numcount = count_num(digits); if (index <= numcount * digits - 1 ) { int num_index = index / digits; int res_index = index % digits; int num = num_in_digits(num_index, digits); res = to_string(num)[res_index] - '0' ; break ; } else { index -= digits * numcount; digits++; } } return res; } int count_num (int digits) { if (digits <= 0 ) { return -1 ; } if (digits == 1 ) { return 10 ; } int count = (int ) (std ::pow (10 , digits - 1 ) + 0.5 ); return 9 * count; } int first_d_num (int digits) { if (digits == 1 ) { return 0 ; } return (int ) (pow (10 , digits - 1 ) + 0.5 ); } int num_in_digits (int index, int digits) { int first = first_d_num(digits); return first + index; }
把数组排成最小的数-45
给一个正整数数组,把它们拼接起来,求得一个最小的数字。
牛客网排成最小的数
1 全排列思路
实际上可以获得所有的排列方法,然后选择最小的。但是这样会比较慢。一共n!
个排列组合。参考全排列 。
2 排序思路
因为实际上最终是,按照一个规则把所有数字排列起来 。
把所有数字转换成字符串
定义一个compare
方法,判断哪个数字应该排列在前面。字符串本身的比较 即可。
对所有数字排序,使用sort(nums.begin(), nums.end(), cmp)
按照排好序的数字来拼接到一个字符串
注意:隐藏的大数问题。用字符串 来保存。
github源代码
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 string array2minnum (const vector <int > &a) { vector <string > nums; for (auto n: a) { nums.push_back(to_string(n)); } sort(nums.begin(), nums.end(), cmp); ostringstream res; for (auto s:nums) { res << s; } return res.str(); } bool cmp (const string & n1, const string & n2) { string n1n2 = n1 + n2; string n2n1 = n2 + n1; if (n1n2 < n2n1) { return true ; } else { return false ; } }
把数字翻译成字符串-46
基本规则:0-a, 1-b, 2-c, ... , 25-z。一个数字(可能是多位数)有多种翻译的可能。给一个数字字符串,求出有多少种翻译方法。比如12258有5种翻译方法。bccfi,bwfi,bczi,mcfi,mzi
1 递归计算
1 - 2258, 12 - 258 。第一个数字有2种翻译方法,再递归地去翻译剩下的部分。
由于存在重复的子问题 ,所以递归不好。从上到下计算也不好。我们要用从最小的子问题自下而上 解决问题
从上到下思考,自下而上循环计算。
设c[i]
是a[i]
开始到末尾的所有翻译种类的数量 。
每个a[i]都能单独翻译,c[i] = c[i+1]
如果a[i]a[i+1]
可以一起翻译,则c[i] += c[i+2]
\[
f(i) =
\begin{cases}
f(i+1) + f(i+2), &a_ia_{i+1} \text{可以一起翻译}\\
f(i+1),&a_i \text{只能单独翻译}\\
\end{cases}
\] github代码
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 int get_trans_count (int number) { string num = to_string(number); if (number < 0 || num.empty()) { return 0 ; } if (num.length() == 1 ) { return 1 ; } vector <int > c(num.size()); for (int i = c.size() - 1 ; i >= 0 ; i--) { bool flag = false ; if (i+1 < c.size()) { int now = num[i] - '0' ; int back = num[i+1 ] - '0' ; int sum = now * 10 + back; if (sum >= 10 && sum <= 25 ) { flag = true ; } } if (i == c.size() - 1 ) { c[i] = 1 ; } else { c[i] = c[i+1 ]; } if (flag == true ) { if (i+2 < c.size()) { c[i] += c[i+2 ]; } else { c[i] += 1 ; } } } return c[0 ]; }
礼物的最大价值-47
m*n的棋盘,每个格子上有一个礼物,每个礼物都有一个价值。从左上角开始走到右下角。每次只能向下-向右走。走一个格子,拿一个礼物。问,最大能拿多大的价值。
设v[i,j]
是格子ij上的价值,设m[i,j]
是到达ij后的总路线最大价值。只能从上面或左边到达。 \[
m[i, j] = \max(m[i-1,j], m[i, j-1]) +v[i, j]
\] 一行一行、一列一列的计算。初始化left和up为0,如果有,则赋值。计算m[i,j] = max(left, up)+v[i,j]
github源码
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 int max_gifts_value (const vector <vector <int >> &v) { if (v.empty() || v[0 ].empty()) { return 0 ; } vector <vector <int >> m; int row = v.size(); int col = v[0 ].size(); for (int i = 0 ; i < row; i++) { vector <int > cur_row(col, 0 ); m.push_back(cur_row); } for (int i = 0 ; i < row; i++) { for (int j = 0 ; j < col; j++) { int up = 0 ; int left = 0 ; if (i > 0 ) { up = m[i-1 ][j]; } if (j > 0 ) { left = m[i][j-1 ]; } m[i][j] = std ::max(up, left) + v[i][j]; } } return m[row-1 ][col-1 ]; }
最长不重复的子字符串-48
给一个字符串,找到里面最长子字符串的长度。
leetcode最长子字符串笔记
第n个丑数-49
leetcode丑数笔记
第一个只出现一次的字符-50
找到字符串中,第一个只出现一次的字符
用空间换时间,HashMap
, 统计出现次数;再遍历字符串,找出现次数为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 char first_char (const string & s) { if (s.empty()) { return '\0' ; } map <char , int > c; for (auto ch:s) { if (c.count(ch) == 0 ) { c[ch] = 1 ; } else { c[ch] += 1 ; } } for (auto ch : s) { if (c[ch] == 1 ) { return ch; } } 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 class Solution {private : map <char , int >m; string s; public : void insert (char ch) { s += ch; if (m.count(ch) == 0 ) { m[ch] = 1 ; } else { m[ch] += 1 ; } } char first_char () { char ch = '#' ; for (auto c:s) { if (m[c] == 1 ) { ch = c; break ; } } return ch; } };