欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

算法(七)剑指offer数组系列

程序员文章站 2022-07-12 09:35:12
...

翻转字符串里的单词

题目

给定一个字符串,逐个翻转字符串中的每个单词。

说明:

无空格字符构成一个 单词 。
输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

题解

先所有字符翻转,然后在单词内翻转(leetcode上要考虑各种情况,其实没必要),示例代码如下:

class Solution {
public:
    string reverseWords(string s) {
        if (s.empty()) {
            return s;
        }

        reverse(s.begin(), s.end());
        int low = 0;
        int high = 0;
        while (low <= s.size() - 1) {
            while (high < s.size() && s[high] != ' ') {
                ++high;
            }
            reverse(s.begin() + low, s.begin() + high);
            low = high + 1;
            high = low;
        }
    
        std::string result;
        result.assign(s.begin() + low_thre, s.begin() + up_thre + 1);
        return result;
    }
};

找出数组中重复的数字(剑指offer_3)

题目

在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了, 也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字2或者3。

题解

如果第i个位置上的元素不是i, 看nums[i]和nums[nums[i]]是否相同,如果相同的话,则证明有重复, 直接返回,否则交换nums[i]和nums[nums[i]],如果交换之后nums[i]和i还是不相同,则 接着重复上述操作,直到把所有元素都遍历完成为止。

示例代码如下:

class Solution {
public:
    int duplication_detection(std::vector<int> nums) {
        if (nums.empty()) {
            return -1;
        }
        
        int length = nums.size();
        for (int i = 0; i < length; ++i) {
            if (nums[i] < 0 || nums[i] >= length) {
                return -1;
            }
        }
        
        for (int i = 0; i < length; ++i) {
            while (nums[i] != i) { // 只要对应位置元素不是对应位置,就继续找
                if (nums[i] == nums[nums[i]]) {
                    return nums[i];
                }

				// 找的手段是交换nums[i]和nums[nums[i]]
                int temp = nums[i];
                nums[i] = nums[temp];
                nums[temp] = temp;
            }
        }
        
        return -1;
    }
};

数组中出现次数超过一半的数字(剑指offer_39)

题目

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

输入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
输出: 2

题解

类似位运算处理,定义times来记录遍历过程中当前遍历元素在之前的遍历过程中出现的次数,初始值为1,定义result为最终结果,在遍历过程中其永远都等于出现次数最多的元素。遍历过程中如果result等于当前遍历元素则++times,否则–times,如果times变为0,则将当前遍历元素赋值给result,times重新赋值为1,遍历结束后返回result即可。

示例代码如下

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int length = nums.size();
        if (length == 0) {
            return -1;
        }

        int result = nums[0];
        int times = 1;
        for (int i = 1; i < length; ++i) {
            if (times == 0) {
                times = 1;
                result = nums[i];
            } else if (result == nums[i]) {
                ++times;
            } else {
                --times;
            }
        }

        return times > 0 ? result : -1; 
    }
};

和为s的连续正数序列(剑指offer_57)

题目

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

示例 1:

输入:target = 9
输出:[[2,3,4],[4,5]]
示例 2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

题解

正常遍历解决方案,示例代码如下

class Solution {
public:
    vector<vector<int>> findContinuousSequence(int target) {
        vector<vector<int>> result;
        if (target <= 0) {
            return result;
        }

        int low = 1;
        int high = 2;
        while (low < high) {
            int cur_sum = (low + high) * (high - low + 1) / 2;
            if (cur_sum > target) {
                ++low;
            } else if (cur_sum == target) {
                vector<int> tmp_result;
                for (int i = low; i <= high; ++i) {
                    tmp_result.emplace_back(i);
                }
                result.emplace_back(tmp_result);
                ++low; // 这里++high和++low效果是一样的
            } else {
                ++high;
            }
        }

        return result;
    }
};

二维数组中的查找(剑指offer_4)

题目

在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

示例:

现有矩阵 matrix 如下:

[
  [1,   4,  7, 11, 15],
  [2,   5,  8, 12, 19],
  [3,   6,  9, 16, 22],
  [10, 13, 14, 17, 24],
  [18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。

题解

由于数组从左到右、从上到下都是递增的,因而可以考虑从右上角开始遍历,如果发现右上角元素小于target,则证明所在行元素都小于target,可以从下面一行开始继续寻找目标;如果右上角元素大于target,则证明所在列元素都大于target,可以从左边一列开始继续寻找目标,示例代码如下所示:

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        if (matrix.empty() || matrix[0].empty()) {
            return false;
        }

        int rows = matrix.size();
        int cols = matrix[0].size();
        int row = 0;
        int col = cols - 1;
        while (row < rows && col >= 0) {
            if (matrix[row][col] > target) {
                --col;
            } else if (matrix[row][col] < target) {
            	// ++row的原因在于当前位置所在行右边的元素已经都比target大了
            	// 但不能保证row+1行col左侧的数据比target大,因而这里需要++row
                ++row; 
            } else {
                return true;
            }
        }

        return false;
    }
};

数组中数字出现的次数(剑指offer_56)

题目

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

示例 1:

输入:nums = [4,1,4,6]
输出:[1,6] 或 [6,1]
示例 2:

输入:nums = [1,2,10,4,1,4,3,3]
输出:[2,10] 或 [10,2]

题解

将数组中所有的元素异或,即可得到只出现一次的两个元素a、b的异或值,可以找到a和b异或值二进制表达值的任意一位为1的位置,数组中这一位为1分为一组,为0分为一组,两组数据分别异或即可得到a和b。示例代码如下所示:

class Solution {
public:
    vector<int> singleNumbers(vector<int>& nums) {
        int ret = 0;
        for (const auto& elem: nums) {
            ret ^= elem;
        }
        int div = 1; // 注意标志位的初始化
        while ((div & ret) == 0) { // & 能够找到只出现一次的两个数二进制表示的第一个不相同位
            div <<= 1;
        }
        int a = 0;
        int b = 0;
        for (const auto& elem: nums) {
            if (elem & div) { // & 能将a和b区分开来,原因同上
                a ^= elem;
            } else {
                b ^= elem;
            }
        }

        return vector<int>{a, b};
    }
};

滑动窗口的最大值(剑指offer_59)

题目

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

题解

直观思路解决问题,具体思路如下:

  1. 构建目标数组max_window_ctnr和滑动过程中维系的滑动窗口 index。
  2. 首先构建初始滑动窗口index,窗口的front永远是当前数组中最大的元素。
  3. 遍历数组位置i直到i到达数组的尾部,遍历过程中完成如下操作:将index的front放入max_window_ctnr,滑动index,具体表现为如果front已经小于等于当前遍历位置i - k,这时弹出front,即可完成窗口的滑动。
  4. 遍历结束后需要把index的front放入max_window_ctnr,因为循环跳出时窗口的最后一个目标元素没有放入max_window_ctnr中。

示例代码如下:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        vector<int> max_window_ctnr;
        if (nums.empty() || k <= 0 || nums.size() < k) {
            return max_window_ctnr;
        }
        deque<int> index;

        // 构建初始窗口阶段,index从头到尾必须是降序的,
        // 且index的头部永远都是当前数组最大值对应的下标,下同
        for (int i = 0; i < k; ++i) {
            while (!index.empty() && nums[i] >= nums[index.back()]) {
                index.pop_back();
            }
            index.push_back(i);
        }

        // 窗口滑动阶段
        for (int i = k; i < nums.size(); ++i) {
            max_window_ctnr.push_back(nums[index.front()]);
            while (!index.empty() && nums[i] >= nums[index.back()]) {
                index.pop_back();
            }
            if (!index.empty() && index.front() <= int(i - k)) {
                index.pop_front();
            }
            index.push_back(i);
        }

		// 最后一个滑动窗口的最大值
        max_window_ctnr.push_back(nums[index.front()]);

        return max_window_ctnr;
    }
};

机器人的运动范围(剑指offer_13)

题目

地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?

示例 1:

输入:m = 2, n = 3, k = 1
输出:3
示例 2:

输入:m = 3, n = 1, k = 0
输出:1
提示:

1 <= n,m <= 100
0 <= k <= 20

题解

本题为<矩阵中的最长递增路径>的简化版,回溯法解决问题,值得注意的是这里定义二维数组visited,记录某个节点是否已经被走过,这样能够加速计算,示例代码如下:

class Solution {
public:
    int movingCount(int m, int n, int k) {
        if (m < 0 || n < 0 || k < 0) {
            return 0;
        }
        vector<vector<bool>> visited(m, vector<bool>(n, false));
        int r = 0;
        int c = 0;

        return  moving_count_core(visited, m, n, r, c, k);
    }

    int count(int row, int col) {
        int result = 0;
        while (row) {
            result += row % 10;
            row /= 10;
        }

        while (col) {
            result += col % 10;
            col /= 10;
        }

        return result;
    }

    int moving_count_core(vector<vector<bool>>& visited, int m, int n, int r, int c, int k) {
        if (r > m - 1 || c > n -1 || count(r, c) > k || visited[r][c]) {
            return 0;
        }
        visited[r][c] = true; // 防止重复计算导致结果变大
        return 1 + moving_count_core(visited, m, n, r + 1, c, k) 
            + moving_count_core(visited, m, n, r, c + 1, k);
    }
};

复杂度

时间:O(mn)
空间:O(mn)

数组中数值和下标相等的元素(剑指offer_53)

题目

假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编程实现一个函数找出数组中任意一个数值等于其下标的元素。例如,在数组{-3, -1, 1, 3, 5}中,数字3和它的下标相等。

题解

常规二分查找解决问题。示例代码如下:

class Solution {
public:
    int findNumber(vector<int>& nums) {
        int length = nums.size();
        if (length == 0) {
            return -1;
        }

        int low = 0;
        int high = length - 1;
        while (low <= high) { // 等号是核心
            int mid = (low + high) / 2;
            if (nums[mid] > mid) {
                high = mid - 1;
            } else if (nums[mid] < mid) {
                low = mid + 1;
            } else {
                return mid;
            }
        }

        return -1;
    }
};

快速排序思想返回数组中位数

题解

快速排序的详解可以参照这里。返回中位数可以通过添加二分法来完成。示例代码如下所示:

int partition(vector<int>& array, int low, int high) {
	int i = low - 1, x = array[high];
	for (int j = low; j <= high - 1; j++) {
		if (array[j] <= x) { // 这里能够显示出快速排序的不稳定性
			swap(array[j], array[++i]);
		}
	}
	swap(array[i + 1], array[high]);
 
	return i + 1;
}

int get_mid(vector<int>& nums) {
    int length = nums.size();
    int low = 0;
    int high = length - 1;
    int result = -1;
    int mid = low + (high - low) / 2;
    while (low <= high) { // 注意等于号的重要性
        int q = partition(nums, low, high);
        if (mid == q) {
            result = mid;
            break;
        } else if (q > mid) {
            high = q - 1;
        } else {
            low = q + 1;
        }
    }
    
    if (result == mid) {
        return nums[mid];
    }
    
    return -1;
}

最小的k个数(剑指offer_40)

题目

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]

题解1 快速排序思想

示例代码如下所示:

class Solution {
public:
    int partition(vector<int>& arr, int start, int end) {
        int length = arr.size();
        if (length == 0 || start < 0 || end >= length) {
            return -1;
        }

        int i = start - 1;
        int x = arr[end];
        for (int j = start; j < end; ++j) {
            if (arr[j] <= x) {
                swap(arr[j], arr[++i]);
            }
        }

        swap(arr[i + 1], arr[end]);

        return i + 1;
    }

    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> result;
        int length = arr.size();
        if (length == 0 || k <= 0) {
            return result;
        }
        if (length <= k) {
            return arr;
        }

        int start = 0;
        int end = length - 1;
        int index = partition(arr, start, end);
        while (index != k - 1) {
            if (index > k - 1) {
                end = index - 1;
                index = partition(arr, start, end);
            } else {
                start = index + 1;
                index = partition(arr, start, end);
            }
        }

        for (int i = 0; i <= index; ++i) {
            result.emplace_back(arr[i]);
        }

        return result;
    }
};

时间复杂度:理想情况下,每次遍历过程中只需要一半元素,因为我们是要找下标为k的元素,第一次切分的时候需要遍历整个数组 (0 ~ n) 找到了下标是 j 的元素,假如 k 比 j 小的话,那么我们下次切分只要遍历数组 (0~k-1)的元素就行啦,反之如果 k 比 j 大的话,那下次切分只要遍历数组 (k+1~n) 的元素就行啦,总之可以看作每次调用 partition 遍历的元素数目都是上一次遍历的 1/2,因此时间复杂度是 N + N/2 + N/4 + … + N/N = N*(2-1/(2^n)) = 2N, 因此时间复杂度是 O(N)。

空间复杂度:O(k),最后存储结果的数组的长度为k。

题解2 堆排序思想

维护一个小顶堆q,先把数组中的前k个元素push到q中,不断地将数组中的元素push到q中,且维护这个大小为k的q,保证q中的元素一直都是当前最小的k个元素,最终q中剩下的元素即为目标元素。示例代码如下所示:

class Solution {
public:
    vector<int> getLeastNumbers(vector<int>& arr, int k) {
        vector<int> result;
        if (k <= 0) {
            return result;
        }
        int length = arr.size();
        if (k >= length) {
            return arr;
        }
        
        priority_queue<int> q;
        for (int i = 0; i < k; ++i) {
            q.push(arr[i]);
        }

        for (int i = k; i < length; ++i) {
            if (q.top() > arr[i]) {
                q.pop();
                q.push(arr[i]);
            }
        }

        for (int i = 0; i < k; ++i) {
            result.emplace_back(q.top());
            q.pop();
        }

        return result;
    }
};

时间复杂度:维护一个大小为k的小顶堆的时间复杂度为O(logk),最坏情况下数组中的元素都要在小顶堆中转一圈,因而总的时间复杂度为O(nlogk)。
空间复杂度:O(k),因为小根堆的大小为k。

数组中的逆序对(剑指offer_51)

题目

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5

题解

归并排序过程中记录待合并的前面数组比后面数组大的情况,示例代码如下所示:

class Solution {
public:
    int reverse_pairs_inner(vector<int>& nums, int low, int high) {
        if (low >= high) {
            return 0;
        }

        int m = (low + high) / 2;
        int result = reverse_pairs_inner(nums, low, m) + reverse_pairs_inner(nums, m + 1, high);
        int i = low;
        int j = m + 1;
        for (int k = low; k <= high; ++k) {
            tmp[k] = nums[k];
        }

        for (int k = low; k <= high; ++k) {
            if (i == m + 1) {
                nums[k] = tmp[j++];
            } else if (j == high + 1 || tmp[i] <= tmp[j]) {
                nums[k] = tmp[i++];
            } else {
                nums[k] = tmp[j++];
                result += m + 1 - i;
            }
        }

        return result;
    }

    int reversePairs(vector<int>& nums) {
        int low = 0;
        int high = nums.size() - 1;
        tmp = vector<int>(nums.size(), 0);
        return reverse_pairs_inner(nums, low, high);
    }
private:
    vector<int> tmp;
};

把数组排成最小的数(剑指offer_45)

题目

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: "102"
示例 2:

输入: [3,30,34,5,9]
输出: "3033459"

题解

此题求拼接起来的最小数字,本质上是一个排序问题。设数组 nums 中任意两数字的字符串为 x 和 y,则规定 排序判断规则 为:

若拼接字符串 x + y > y + x,则 x “大于” y ;
反之,若 x + y < y + x,则 x “小于” y;

用上述规则进行排序即可得到结果,示例代码如下所示:

class Solution {
public:
    void q_s(vector<string>& nums, int l, int r) {
        if (l >= r) {
            return;
        }
        int i = l;
        int j = r;
        while (i < j) {
        	// 必须先右后左,这样才能保证i和j遍历过程是正常的
            while (i < j && nums[j] + nums[l] >= nums[l] + nums[j]) {
                --j;
            }
            while (i < j && nums[i] + nums[l] <= nums[l] + nums[i]) {
                ++i;
            }
            swap(nums[i], nums[j]);
        }
        swap(nums[i], nums[l]);
        q_s(nums, l, i - 1);
        q_s(nums, i + 1, r);
    }

    string minNumber(vector<int>& nums) {
        string result;
        int length = nums.size();
        if (length == 0) {
            return result;
        }

        vector<string> tmp;
        for (auto& num: nums) {
            tmp.emplace_back(to_string(num));
        }

        q_s(tmp, 0, length - 1);
        for (auto& str: tmp) {
            result.append(str);
        }

        return result;
    }
};

调整数组顺序使奇数位于偶数前面

题目

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。

示例:

输入:nums = [1,2,3,4]
输出:[1,3,2,4] 
注:[3,1,2,4] 也是正确的答案之一。

题解

双指针解决问题,示例代码如下所示:

class Solution {
public:
    vector<int> exchange(vector<int>& nums) {
        int length = nums.size();
        if (length == 0) {
            return vector<int>();
        }

        int begin = 0;
        int end = length - 1;
        while (begin < end) {
            if (nums[begin] % 2 == 1) {
                ++begin;
                continue;
            }
            if (nums[end] % 2 == 0) {
                --end;
                continue;
            }
            swap(nums[begin++], nums[end--]);
        }

        return nums;
    }
};

合并排序的数组(常见面试)

题目

给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。

初始化 A 和 B 的元素数量分别为 m 和 n。

示例:
输入:
A = [1,2,3,0,0,0], m = 3
B = [2,5,6],       n = 3

输出: [1,2,2,3,5,6]

题解

从后向前赋值,这样能够保证不占用额外的空间复杂度。示例代码如下所示:

class Solution {
public:
    void merge(vector<int>& A, int m, vector<int>& B, int n) {
        int pa = m - 1;
        int pb = n - 1;
        int tail = m + n - 1;
        int cur;
        while (pa >= 0 || pb >= 0) {
            if (pa == -1) {
                cur = B[pb--];
            } else if (pb == -1) {
                cur = A[pa--];
            } else if (A[pa] > B[pb]) {
                cur = A[pa--];
            } else {
                cur = B[pb--];
            }
            A[tail--] = cur;
        }
    }
};
相关标签: 算法 算法 c++