diff --git a/algorithms/DFS/demo.cc b/algorithms/DFS/demo.cc index c0550c8..4ba47e6 100644 --- a/algorithms/DFS/demo.cc +++ b/algorithms/DFS/demo.cc @@ -42,6 +42,7 @@ void DFS(Tree root){ void BFS(Tree root){ //广度优先遍历, 使用队列 + // NOTE 1, 时间复杂度o(n),空间复杂度不是o(1),而是o(logn),因为队列内存储的最大的情况是存储二叉树的全部叶子节点 queue node_list; node_list.push(root); while(!node_list.empty()){ diff --git a/algorithms/Linked_List/Reverse_Linked_List.cpp b/algorithms/Linked_List/Reverse_Linked_List.cpp new file mode 100644 index 0000000..1463e5f --- /dev/null +++ b/algorithms/Linked_List/Reverse_Linked_List.cpp @@ -0,0 +1,37 @@ +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * ListNode *next; + * ListNode(int x) : val(x), next(NULL) {} + * }; + */ +class Solution { +public: + // method 1, 暴力法,时间复杂度o(2*n),空间复杂度o(n) + // 遍历两次单链表,通过中间数组变量存储val来修改链表的值 + // 第一次遍历,利用一个中间数组变量来存储不同节点val + // 第二次遍历,利用中间变量数组来修改链表的值 + + // method 2, 指针法, 时间复杂度o(n), 空间复杂度o(1) + // 反转指针的指向, 遍历单链表,利用中间节点变量存储下一个节点,再反转next指针 + ListNode* reverseList(ListNode* head) { + // 处理特例 + if (NULL == head || NULL == head->next)return head; + // 定义三元组, + ListNode* left = NULL; + ListNode* mid = head; + ListNode* right = mid->next; + while (NULL != right){ + // 反转指针 + mid->next = left; + // 滑动三元组,先后顺序不要错了 + left = mid; + mid = right; + right = right->next; + } + // 滑动到最右端的三元组(倒数第二个节点,倒数第一个节点,NULL),没有做mid->next赋值,这里补上 + mid->next = left; + return mid; + } +}; diff --git a/algorithms/Linked_List/picture/Reverse Linked List.png b/algorithms/Linked_List/picture/Reverse Linked List.png new file mode 100644 index 0000000..029fdae Binary files /dev/null and b/algorithms/Linked_List/picture/Reverse Linked List.png differ diff --git a/algorithms/Linked_List/picture/readme.md b/algorithms/Linked_List/picture/readme.md new file mode 100644 index 0000000..7898192 --- /dev/null +++ b/algorithms/Linked_List/picture/readme.md @@ -0,0 +1 @@ +a diff --git a/algorithms/Linked_List/readme.md b/algorithms/Linked_List/readme.md new file mode 100644 index 0000000..c82cf1c --- /dev/null +++ b/algorithms/Linked_List/readme.md @@ -0,0 +1,5 @@ +# 理论 +# 特点及应用场景 +# Leetcode +simple-206-Reverse Linked List +![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-4/algorithms/Linked_List/picture/Reverse%20Linked%20List.png) diff --git a/algorithms/dynamic_planning/300. Longest Increasing Subsequence.cpp b/algorithms/dynamic_planning/300. Longest Increasing Subsequence.cpp new file mode 100644 index 0000000..3539494 --- /dev/null +++ b/algorithms/dynamic_planning/300. Longest Increasing Subsequence.cpp @@ -0,0 +1,80 @@ +class Solution { +public: + // // method 1,动态规划,时间复杂度o(n*n),空间复杂度o(n) + // // 动态方程是dp[n + 1] = dp[n](if nums[n] <= max) or dp[n] + 1(if nums[n] > max) + // // 状态定义:自变量为nums的索引n,应变量是以nums[n]作为子序列的尾部元素条件下(这个限定条件很重要) + // // 所能构建的最长上升子序列长度,注意这里应变量并不是0~n的数组对应的最大上升子序列长度,所以最终结果是dp数组最大值元素 + // // 初始状态:初始为1,因为nums[0] > INT_MIN,所以dp[0] = 1; + // int lengthOfLIS(vector& nums) { + // // 处理特殊case + // int max_len = 0; + // if (nums.size() == 0)return 0; + // vector dp(nums.size(), 1); + // for (size_t i = 0; i < nums.size(); ++i){ + // // 如果nums[i] > max,则把nums[i]添加到上一个最长上升子序列之后,成为新的最长上升子序列 + // for (size_t j = 0; j < i; ++j){ + // // NOTE 1(important), 动态规划的动态方程的等号右边可以是无数个变量 + // // 即dp[n + 1] = f(dp[0], d[1],..., dp[n]) + // if (nums[j] < nums[i])dp[i] = max(dp[i], dp[j] + 1); + // // NOTE 2,这里不要写下面一句话,因为动态方程的限定条件是以nums[i]结尾的最长上升子序列 + // //else dp[i] = max(dp[i], dp[j]); + // } + // max_len = max_len > dp[i]? max_len: dp[i]; + // } + // return max_len; + // } + + // method 2, 动态规划 + 二分搜索,时间复杂度o(n*logn),空间复杂度o(n) + // 第一种方法的时间复杂度较高,方法二的时间复杂度更低,两者的差别在于遍历每一个动态方程时的时间复杂度可以从o(n)->o(logn) + // 1,状态,采用tail数组来存储,自变量是当前nums子数组的最大上升子序列长度len - 1 + // 应变量是对应的长度为len - 1的上升子序列(存在多个)的尾部元素的最小值 + // 2,动态方程if (nums[i]>tail[-1])tail.push_back[nums[i]]; + // else tail[j] = nums[i] where j is the index of first element > nums[i] + // 2,构造一个新的数据结构,以便存储历史元素大小关系,即tail数组,包含两部分,下标,value。 + // 下标是是当前nums子数组的最大上升子序列长度len,value是对应的长度为len的上升子序列(存在多个)的尾部元素的最小值 + // 更新tail数组的法是:比较nums[i]和tail数组,采用二分搜索法,按照从左到右的顺序,找到tail数组中tail[j] + // tail[j]是第一个大于nums[i]的元素 + // 分为两种情况,A,如果能找到,更新历史的最长上升子序列长度==j对应的尾部元素,即tail[j] = nums[i] + // B,如果不能找到,更新当前的最长上升子序列,即tail.push_back[nums[i]] + // 3,tail数组的长度即为最大上升子序列长度 + // 4, 初始状态 + + // 4, 可以思考一下为什么要采用二分搜索法,按照从左到右的顺序,替换tail[j](第一个大于nums[i]) = nums[i]?这是问题的关键 + // 事实上,这是为了满足动态方程的应变量是对应的长度为len的上升子序列(存在多个)的尾部元素的最小值,因为 + // A,tail[0]到tail[j - 1]的值(即长度为1到j的上升子序列的尾部元素值)都比nums[i]小,所以不用更新尾部元素 + // B,tail[j + 1]到tail[-1]的尾部元素如果用nums[i]来更新,会使得对应序列不再是上升子序列 + // C, tail[j]对应了长度为j+1的上升子序列的尾部元素值为tail[j],而nums[i]>tail[j-1] && nums[i] < tail[j],所以新的上升子序列(tail[j - 1]对应的上升子序列,拼接上nums[i])才是尾部元素最小的长度为j+1的上升子序列 + int binary_search(const vector& tail, int val){ + // 返回第一个大于val的元素对应的索引index + int i = 0; + int j = tail.size() - 1; + int m; + if (tail[0] > val)return 0; + while (i <= j){ + m = (i + j) / 2; + // NOTE 1, 一定注意了,这里i=m+1,而不是i=m,否则容易陷入死循环 + if (tail[m] < val)i = m + 1; + else if (tail[m] > val)j = m - 1; + // 如果存在相等的元素索引 + else return m; + } + // 如果不存在相等的元素,返回第一个更大的元素索引,这里不确定是tail[m]还是tail[m+1]第一个大于val,所以比较一次 + if (tail[m] > val)return m; + else return m + 1; + } + int lengthOfLIS(vector& nums) { + // 处理特殊case + //nums.resize(2,2); + if (nums.size() == 0)return 0; + // 初始化动态方程存储结构 + vector tail(1, nums[0]); + for (size_t i = 1; i < nums.size(); ++i){ + // 大于,则当前最长上升子序列加1 + if (nums[i] > tail[tail.size() - 1])tail.push_back(nums[i]); + // 不大于(由于题意说了不重复,不存在等于情况),则更新历史某一个最长上升子序列的尾部元素 + int index = binary_search(tail, nums[i]); + tail[index] = nums[i]; + } + return tail.size(); + } +}; diff --git a/algorithms/dynamic_planning/377. Combination Sum IV.cc b/algorithms/dynamic_planning/377. Combination Sum IV.cc new file mode 100644 index 0000000..7b8a17b --- /dev/null +++ b/algorithms/dynamic_planning/377. Combination Sum IV.cc @@ -0,0 +1,32 @@ +class Solution { +public: + // method 1,动态规划,非递归 + // 首先,该题明显需要做很多重复计算 + // 其次,可以分析出最优子结构,即动态方程为函数映射,以和为自变量,以和的组合的个数为应变量。 + // dp(sum) = sum([i for i in nums if i >=0]), + // 其中,叶子节点的值,dp[0] = 1, dp[负数]=0;主要因为数组和target都是正整数,非正整数不能这样初始化叶子节点的值 + int combinationSum4(vector& nums, int target) { + // 异常输入 + assert(target > 0); + // NOTE 1, 一定要存储double,否则int可能会溢出。比如输入为[3, 33, 333],10000 + // 定义动态方程存储结构 + vector dp; + // 初始化叶子节点的值 + dp.push_back(1); + // 遍历动态方程的自变量 + for (size_t sum = 1; sum <= target; sum ++){ + int n = 0; + for (size_t i = 0; i < nums.size(); ++i){ + int diff = sum - nums[i]; + if (diff < dp.size() && diff >=0){ + double add = double(n) + dp[diff]; + // 由于返回值为int,因而按照题意不可能超过int类型,所以这里做个阈值截断 + if (add > INT_MAX)n = INT_MAX; + else n += dp[diff]; + } + } + dp.push_back(n); + } + return dp[target]; + } +}; diff --git a/algorithms/dynamic_planning/picture/Combination_Sum_IV.png b/algorithms/dynamic_planning/picture/Combination_Sum_IV.png new file mode 100644 index 0000000..30034f8 Binary files /dev/null and b/algorithms/dynamic_planning/picture/Combination_Sum_IV.png differ diff --git a/algorithms/dynamic_planning/picture/Longest_Increasing_Subsequence.png b/algorithms/dynamic_planning/picture/Longest_Increasing_Subsequence.png new file mode 100644 index 0000000..cc0381b Binary files /dev/null and b/algorithms/dynamic_planning/picture/Longest_Increasing_Subsequence.png differ diff --git a/algorithms/dynamic_planning/readme.md b/algorithms/dynamic_planning/readme.md index 8c0d541..660709a 100644 --- a/algorithms/dynamic_planning/readme.md +++ b/algorithms/dynamic_planning/readme.md @@ -3,6 +3,13 @@ # Leetcode simple-53-最大子序和 ![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-3/algorithms/dynamic_planning/picture/Maximum_Subarray.png) + simple-746-使用最小花费爬楼梯 ![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-3/algorithms/dynamic_planning/picture/Min_Cost_Climbing_Stairs.png) +medium-300-最长上升子序列 +![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-3/algorithms/dynamic_planning/picture/Longest_Increasing_Subsequence.png) + +medium-377-组合总数 IV +![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-3/algorithms/dynamic_planning/picture/Combination_Sum_IV.png) + diff --git a/algorithms/sort/H_index.cpp b/algorithms/sort/H_index.cpp new file mode 100644 index 0000000..9be1ef7 --- /dev/null +++ b/algorithms/sort/H_index.cpp @@ -0,0 +1,77 @@ +class Solution { +public: + // method 1, 排序算法, 时间复杂度o(n*logn), 空间复杂度o(1) + // 什么排序算法都可以,这里特别地,采用堆排序,平时堆排序的场景少,刚好演练一下 + // 1, 堆排序,原地正排序 + // 2, 从小到大开始遍历数组size N,直到找到一个元素位置index, 第一次出现N-index& citations, int k, int i){ + int tmp = citations[k]; + citations[k] = citations[i]; + citations[i] = tmp; + } + + // 从上往下更新子树,根节点为k,尾端为end + void to_down(vector& citations, int k, int end){ + int index; + // NOTE 5, 索引是<= end,不是< end + while (2 * k + 1 <= end){ + index = 2 * k + 1; + if (index + 1 <= end && citations[index] < citations[index + 1])index ++; + if (citations[index] > citations[k]){ + swap(citations, index, k); + k = index; + }else break; + } + } + + // 从底向上更新 + void to_top(vector& citations){ + // NOTE 1, 第一个非叶子节点的索引是citations.size() / 2 - 1,但只能保证有左孩子节点 + for (int i = citations.size() / 2 - 1; i >= 0; --i){ + // 初始化交换的孩子节点索引 + int k = 2 * i + 1; + // 比较左右孩子节点大小 + // NOTE 2, 判断是有右孩子节点,防止k+1数组越界 + if (k + 1 < citations.size() && citations[k] < citations[k + 1])k += 1; + // 比较父节点和孩子节点大小 + if (citations[k] > citations[i]){ + swap(citations, k, i); + to_down(citations, k, citations.size() - 1); + } + } + } + + // 构建大顶堆,堆的初始化 + void init_heap(vector& citations){ + to_top(citations); + } + + void update_heap(vector& citations, int end){ + // 先把堆顶元素放到数组尾端 + // NOTE 5, 索引是end + swap(citations, 0, end); + // 从上到下更新堆 + // NOTE 5, 索引是end - 1 + to_down(citations, 0, end - 1); + } + + int hIndex(vector& citations) { + init_heap(citations); + // NOTE 2, j >=0, 不是j>0,避免输入为[1], 输出为0 + int res = 0; + for (int j = citations.size() - 1; j >= 0; --j){ + // NOTE 5, 索引是j + update_heap(citations, j); + int nums = citations.size() - j; + // NOTE 3, 注意返回条件,每次更新h-index,直到citations[j] < nums跳出循环 + if (citations[j] >= nums)res = nums; + else return res; + } + return res; + } +}; diff --git a/algorithms/sort/picture/H_index.png b/algorithms/sort/picture/H_index.png new file mode 100644 index 0000000..8fc69fb Binary files /dev/null and b/algorithms/sort/picture/H_index.png differ diff --git a/algorithms/sort/readme.md b/algorithms/sort/readme.md index e0762c4..483c3c4 100644 --- a/algorithms/sort/readme.md +++ b/algorithms/sort/readme.md @@ -10,3 +10,6 @@ ## medium-215-数组中的第K个最大元素 ## medium-148-排序链表 ![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-2/algorithms/sort/picture/Sort_List.png) +## meidium-274-H指数 +![image](https://github.com/PseudoProgrammer/leetcode_analysis/blob/PseudoProgrammer-patch-2/algorithms/sort/picture/H_index.png) + diff --git "a/nature/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\346\234\254\350\264\250\346\230\257\344\273\200\344\271\210" "b/nature/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\346\234\254\350\264\250\346\230\257\344\273\200\344\271\210" new file mode 100644 index 0000000..ce4899a --- /dev/null +++ "b/nature/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\347\232\204\346\234\254\350\264\250\346\230\257\344\273\200\344\271\210" @@ -0,0 +1,46 @@ +数据结构和算法的本质是什么 +数据结构和算法的本质是为了利用机器来实现人们想要的逻辑,并以此来降低重复性劳动力的成本,主要包含信息的存储、计算、传递三大作用,也是互联网大产业发展的根基。以通信为例,我们来阐述其价值 +1)很久很久以前,传递成本很高,人们传递信息需要走门串巷,如果我想跟暗恋对象表白,那我就需要走很久的路,去她家里告诉她,或者借着她亲哥来我家串门时,委托她亲哥回家告诉她明天晚上7点,在村里那颗老树下约会 +2)后来有了鸽子,减少了传递成本,人们可以不用徒步去她家了,可以通过飞鸽传书借助鸽子来传递信息 +3)后来有了计算器,减少了计算成本,大家不用口算大数,或者硬生生用捉襟见肘的十个手指来算,直接用计算器,简单又准确 +3)后来有了硬盘,减少了存储成本,以前写情书,写在纸上又扔掉,攒了一大箩筐的草稿,现在只需要敲键盘,可以存几亿封情书,还可以随意修改不浪费纸张 +4)后来有了U盘,减少了传递(沟通也是一种更加深度的传递)成本,你想要动作片,来我家拿U盘即可,你自己看,我就不跟你细聊了 +那么我们对所有的数据都用数组来存储是不是可以? +当然也可以,但是效率很低,比如单向链表类数据用数组来存储,也是可以的,单向链表有next指针,数组有index,存储空间复杂度差不多,但是如果我要删除其中一个元素,数组就会先得很臃肿而低效 +,因为数组是连续性存储空间,删除一个元素,需要把右边的元素都通通往前做一遍,和删除元素的左边元素拼接上,再形成连续性存储空间,如果是链表的话,就很高效,删除这个节点就可以了,右边节点不用挪 +因此,为了提高存储、计算、传递效率,我们需要不同的数据结构来适配不同的逻辑实现, +比如为了提高存储效率,我们可以把复杂长串的字母数据用数字-字母映射表来转存为数字,存储大小大幅下降,最后再通过映射表反向翻译即可 +比如为了提高计算效率,我们可以用int代替long来计算常规数字的计算场景 +比如为了提高传递效率,谷歌设计了protobuffer数据结构,把数据全部编码为整型数字后传输到目的地,再利用protobuffer翻译回源数据,大幅度减少了通信成本 + +刷leetcode的最高境界是通过刷题加深我们对常见逻辑和更匹配的数据结构的理解,如果利用现有数据结构来实现解题逻辑,时间复杂度和空间复杂度都很高,那怎么办? +一种办法是使用更加高效的数据结构,另一种办法是我们转换解题逻辑,这样就打开了视野格局,在更高层面上审视问题,现有数据结构也能更高效解题了 +题目是多种多样,无穷无尽的,但是数据结构相对来说比较有限,我们来研究下不同数据结构的优劣势,适配的本质场景,进而可以帮助我们打开更多样、更高效的解题思路 + +上层 + +树(二叉搜索树):对树形结构效率更高,数组只是一维的 +哈希表:查找效率更高,比排序数组还要高,o(1)时间复杂度 +映射表:kv数据库,也可以用两个数组来解决,但是两个数组的效率低; +堆:小顶堆/大顶堆,找出第k大元素,前k大元素列表 +栈:后进先出LIFO,底层可以用数组实现也可以用链表实现,同时栈也更方便动态维护,如果用链表方式的增删改效率高 +队列:先进先出FIFO,底层可以用数组实现也可以用链表实现 +优先队列 +二位数组(二位有序数组) + +中层 +链表:查找元素效率低(o(n)),邻域查找效率高(查找next元素),增删改元素效率高(o(1)),内存利用效率高(可以用碎片内存),可动态拓展 +数组:查找效率高(o(1)), 邻域查找效率高,增删改查元素效率低(o(n)),内存利用效率低,不支持动态拓展 +排序数组:数组的一种特殊方式,比如用来做高效的二分查找,时间复杂度log(n),比数组查找效率更高 + +下层 +int:单个整型元素,也可以用一个元素的数组来存储,但是效率低 +char:单个字符 + +底层 +二进制:只有0/1值,很简单,可以用于快速幂,快速乘等方法 + +应用 +哈希表和动态规划都是为了避免重复遍历来减少时间复杂度 + +