一. 问题
给定含 n 个整数的数组,找出连续的有序最长子数组。算法的运行时间是多少?
二. 思路
假设有一个序列, data = (1, 2, 3, 4, 2, 1, 3, 0),显然,从元素 1 到元素 4 是升序排列的,并且有 4 个元素。我们为了简单起见,仅查找升序排列的子序列。
要获得子序列的情况,我们需要三个信息:(1)子序列开始下标(2)子序列结尾下标(3)子序列长度。并且还需要三个辅助变量:(1)中间结果子序列开始下标(2)中间结果子序列结束下标(3)中间结果子序列长度。
我们从首元素开始,依次向后比较,同时用上面提到的辅助变量记住需要的下标,如果满足条件,则一直向后推;如果不满足条件,则将这些下标记录在结果中。再重置辅助变量,直到比较完所有元素。因为有升序这一条件,我们便可以保证操作不用回退,即一路推下去。
三. 代码实现
1 vector<int> max_ordered_sub_sequence(const vector<int>& data) { 2 vector<int> result; 3 int start_index = 0, end_index = 0, sequence_len = 1; 4 int temp_len = 1; 5 6 for (int temp_start = 0, temp_end = 1; temp_end < data.size(); ++temp_end) { 7 int k = temp_end - 1; 8 if (data[k] <= data[temp_end]) { 9 ++temp_len; 10 continue; 11 } 12 13 if (temp_len > sequence_len) { 14 sequence_len = temp_len; 15 start_index = temp_start; 16 end_index = temp_end -1 ; 17 temp_start = temp_end; 18 temp_len = 1; 19 } else { 20 temp_start = temp_end; 21 temp_len = 1; 22 } 23 } 24 25 result.push_back(sequence_len); 26 result.push_back(start_index); 27 result.push_back(end_index); 28 29 return result; 30 }
(1)代码分析
第 2 到 4 行,我们进行变量初始化。只有一个元素的时候,前后下标一致,此时子序列中含有 1 个元素。第 8 到 11 行,元素如果升序,则向下推。第 13 到 18 行,算法执行到这里,说明元素已经出现了逆序,并且当前子序列比原来的子序列更长,那么我们要把辅助变量记住,然后进行重置。第 19 到 22 行说明,虽然出现了逆序,但是当前子序列的长度,并没有超过我们已经记录的子序列,此时只需要重置辅助变量。第 25 到 27 行,执行到这里,说明算法执行完毕,将结果放到 vector 中返回即可。
(2)算法正确性证明
我们一开始考虑,若序列只包含一个元素,那么最长子序列就是它本身。如果序列长度大于 1,那么进行比较。我们记录的下标只有在需要的时候才会进行更新,也就是逆序的时候。而按照升序排列,当前子序列最后一个元素,一定是比该序列前面元素大(可以相等),走到现在产生了逆序,说明这个新元素比子序列所有元素都要大,此时不用回退,而是可以直接将辅助变量的开始下标,拉到结束位置。如果到达末尾,那么结果必将是所求的最长子序列。算法只遍历一遍序列,显然,时间复杂度是 O(n)。
(3)额外优化
我们再更进一步,仔细思考一个小问题:假如前一个子序列是最长的,它的长度过了整个序列的一半。此时它突然遇到一个逆序。这个时候,我们可以直接返回结果,因为后一半元素,就算也是有序的,长度也肯定不会大于前面的子序列。