数据结构专题之数组和字符串

介绍

数组是数据结构中的基本模块之一。因为字符串是由字符数组形成的,所以二者是相似的。

数组

数组是一种基本的数据结构,用于按顺序存储元素的集合。但是元素可以随机存取,因为数组中的每个元素都可以通过数组索引来识别。

数组可以有一个或多个维度。这里我们从一维数组开始,它也被称为线性数组。

动态数组

数组具有固定的容量,我们需要在初始化时指定数组的大小,但是有时候,我们并不知道要定义多大的数组,所以很不方便
因此,大多数编程语言都提供内置的动态数组,它仍然是一个随机存取的列表数据结构,但大小是可变的。例如,在 C++ 中的 vector,以及在 Java 中的 ArrayList

#include <iostream>

int main() {
    // 1. initialize
    vector<int> v0;
    vector<int> v1(5, 0);
    // 2. make a copy
    vector<int> v2(v1.begin(), v1.end());
    vector<int> v3(v2);
    // 2. cast an array to a vector
    int a[5] = {0, 1, 2, 3, 4};
    vector<int> v4(a, *(&a + 1));
    // 3. get length
    cout << "The size of v4 is: " << v4.size() << endl;
    // 4. access element
    cout << "The first element in v4 is: " << v4[0] << endl;
    // 5. iterate the vector
    cout << "[Version 1] The contents of v4 are:";
    for (int i = 0; i < v4.size(); ++i) {
        cout << " " << v4[i];
    }
    cout << endl;
    cout << "[Version 2] The contents of v4 are:";
    for (int& item : v4) {
        cout << " " << item;
    }
    cout << endl;
    cout << "[Version 3] The contents of v4 are:";
    for (auto item = v4.begin(); item != v4.end(); ++item) {
        cout << " " << *item;
    }
    cout << endl;
    // 6. modify element
    v4[0] = 5;
    // 7. sort
    sort(v4.begin(), v4.end());
    // 8. add new element at the end of the vector
    v4.push_back(-1);
    // 9. delete the last element
    v4.pop_back();
}
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        // 1. initialize
        List<Integer> v0 = new ArrayList<>();
        List<Integer> v1;                           // v1 == null
        // 2. cast an array to a vector
        Integer[] a = {0, 1, 2, 3, 4};
        v1 = new ArrayList<>(Arrays.asList(a));
        // 3. make a copy
        List<Integer> v2 = v1;                      // another reference to v1
        List<Integer> v3 = new ArrayList<>(v1);     // make an actual copy of v1
        // 3. get length
        System.out.println("The size of v1 is: " + v1.size());;
        // 4. access element
        System.out.println("The first element in v1 is: " + v1.get(0));
        // 5. iterate the vector
        System.out.print("[Version 1] The contents of v1 are:");
        for (int i = 0; i < v1.size(); ++i) {
            System.out.print(" " + v1.get(i));
        }
        System.out.println();
        System.out.print("[Version 2] The contents of v1 are:");
        for (int item : v1) {
            System.out.print(" " + item);
        }
        System.out.println();
        // 6. modify element
        v2.set(0, 5);       // modify v2 will actually modify v1
        System.out.println("The first element in v1 is: " + v1.get(0));
        v3.set(0, -1);
        System.out.println("The first element in v1 is: " + v1.get(0));
        // 7. sort
        Collections.sort(v1);
        // 8. add new element at the end of the vector
        v1.add(-1);
        v1.add(1, 6);
        // 9. delete the last element
        v1.remove(v1.size() - 1);
    }
}
经典案例
寻找数组的中心索引

给定一个整数类型的数组 nums,请编写一个能够返回数组“中心索引”的方法。

我们是这样定义数组中心索引的:数组中心索引的左侧所有元素相加的和等于右侧所有元素相加的和。

如果数组不存在中心索引,那么我们应该返回 -1。如果数组有多个中心索引,那么我们应该返回最靠近左边的那一个。

示例 1:

输入:
nums = [1, 7, 3, 6, 5, 6]
输出: 3
解释:
索引3 (nums[3] = 6) 的左侧数之和(1 + 7 + 3 = 11),与右侧数之和(5 + 6 = 11)相等。
同时, 3 也是第一个符合要求的中心索引。

示例 2:

输入:
nums = [1, 2, 3]
输出: -1
解释:
数组中不存在满足此条件的中心索引。

说明:

  • nums 的长度范围为 [0, 10000]
  • 任何一个 nums[i] 将会是一个范围在 [-1000, 1000]的整数。
class Solution {
    public int pivotIndex(int[] nums) {
        int left = 0;
        for(int i = 0; i < nums.length; i++) {
            int right = 0;
            left = i - 1 < 0 ? 0 : left + nums[i - 1];
            for(int j = i + 1; j < nums.length; j++) {
                right += nums[j]; 
            }
            if(left == right) {
                return i;
            }
        }       
        return -1;
    }
}
至少是其他数字两倍的最大数

在一个给定的数组nums中,总是存在一个最大元素 。

查找数组中的最大元素是否至少是数组中每个其他数字的两倍。

如果是,则返回最大元素的索引,否则返回-1。

示例 1:

输入: nums = [3, 6, 1, 0]
输出: 1
解释: 6是最大的整数, 对于数组中的其他整数,
6大于数组中其他元素的两倍。6的索引是1, 所以我们返回1.

示例 2:

输入: nums = [1, 2, 3, 4]
输出: -1
解释: 4没有超过3的两倍大, 所以我们返回 -1.

提示:

  • nums 的长度范围在[1, 50].
  • 每个 nums[i] 的整数范围在 [0, 100].
/**
 * 一次遍历找到最大值和第二大的值,然后判断第一大是否超过第二大的两倍
 */
class Solution {
    public int dominantIndex(int[] nums) {
        int max1 = -1, max2 = -1, maxIndex = -1;
        for(int i = 0; i < nums.length; i++) {
            if(nums[i] > max1) {
                max2 = max1;
                max1 = nums[i];
                maxIndex = i;
            } else if (nums[i] > max2) {
                max2 = nums[i];
            }
        }
        if(max1 >= max2 * 2) {
            return maxIndex;
        }
        return -1;
    }
}
加一

给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例 1:

输入: [1,2,3]
输出: [1,2,4]
解释: 输入数组表示数字 123。

示例 2:

输入: [4,3,2,1]
输出: [4,3,2,2]
解释: 输入数组表示数字 4321。

//golang语言
func plusOne(digits []int) []int {
    //length := len(digits)
    var res []int 
    carry := 1
    for i := len(digits) - 1; i >= 0 && carry > 0; i-- {
        sum := digits[i] + carry
        digits[i] = sum % 10
        carry = sum / 10
    }
    if carry > 0 {
        res = append(res, 1)
    }
    for j := 0; j < len(digits); j++ {
        res = append(res, digits[j])
    }
    return res
}
二维数组

类似于一维数组,二维数组也是由元素的序列组成。但是这些元素可以排列在矩形网格中而不是直线上。

经典案例
对角线遍历

给定一个含有 M x N 个元素的矩阵(M 行,N 列),请以对角线遍历的顺序返回这个矩阵中的所有元素,对角线遍历如下图所示。

示例:

输入:

[
   [ 1, 2, 3 ],
   [ 4, 5, 6 ],
   [ 7, 8, 9 ]
]

输出: [1,2,4,7,5,3,6,8,9]

解释:
在这里插入图片描述
说明:

  • 给定矩阵中的元素总数不会超过 100000 。
class Solution {
    public int[] findDiagonalOrder(int[][] matrix) {
        int m = matrix.length;
        if(m == 0) {
            return new int[]{};
        }
        int n = matrix[0].length;
        int[] res = new int[m * n];
        int index = 0;
        int i = 0, j = 0;
        for(int l = 0; l < m * n; l++) {
            res[index] = matrix[i][j];
            index++;
            if((i + j) % 2 == 0) {
                if(j == n - 1) {
                    i++;
                } else if (i == 0) {
                    j++;
                } else {
                    i--; 
                    j++;
                }
                
            } else {
                if(i == m - 1) {
                    j++;
                } else if (j == 0) {
                    i++;
                } else {
                    i++; 
                    j--;
                }
            }
        }
        return res;
    }
}
螺旋矩阵

给定一个包含 m x n 个元素的矩阵(m 行, n 列),请按照顺时针螺旋顺序,返回矩阵中的所有元素。

示例 1:

输入:
[
    [ 1,  2,  3 ],
    [ 4,  5,  6 ],
    [ 7,  8,  9 ]
]
输出: [1,2,3,6,9,8,7,4,5]

示例 2:

输入:
[
     [1,  2,  3,  4],
     [5,  6,  7,  8],
     [9,10,11,12]
]
输出: [1,2,3,4,8,12,11,10,9,5,6,7]

class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> res = new ArrayList();
        int m = matrix.length;
        if(m == 0) {
            return res;
        }
        int n = matrix[0].length;
        
        int index = 0;
        int up = 0, down = m - 1, left = 0, right = n - 1;
        while(true) {
            for(int j = left; j <= right; j++) {
                res.add(matrix[up][j]);
            }
            if(++up > down) {
                break;
            }
            for(int i = up; i <= down; i++) {
                res.add(matrix[i][right]);
            }
            if(--right < left) {
                break;
            }
            for(int j = right; j >= left; j--) {
                res.add(matrix[down][j]);
            }
            if(--down < up) {
                break;
            }
            for(int i = down; i >= up; i--) {
                res.add(matrix[i][left]);
            }
            if(++left > right) {
                break;
            }
        }
        return res;
    }
}
杨辉三角

给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。

https://upload.wikimedia.org/wikipedia/commons/0/0d/PascalTriangleAnimated2.gif

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:

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

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> res = new ArrayList();
        if(numRows == 0) {
            return res;
        }
        List<Integer> ls = new ArrayList();
        ls.add(1);
        res.add(ls);
        
        if(numRows == 1) {
            return res;
        }
        for(int i = 1; i < numRows; i++) {
            ls = new ArrayList();
            ls.add(1);
            for(int j = 1; j <= i - 1; j++) {
                ls.add(res.get(i - 1).get(j - 1) + res.get(i - 1).get(j));
            }
            ls.add(1);
            res.add(ls);
        }
        return res;
    }
}
字符串

字符串实际上是一个 unicode 字符数组

比较函数

字符串有它自己的比较函数
然而,存在这样一个问题:

我们可以用 “==” 来比较两个字符串吗?

这取决于下面这个问题的答案:

我们使用的语言是否支持运算符重载

  1. 如果答案是 yes (例如 C++)。我们可以使用 “==” 来比较两个字符串。
  2. 如果答案是 no (例如 Java),我们可能无法使用 “==” 来比较两个字符串。当我们使用 “==” 时,它实际上会比较这两个对象是否是同一个对象。
#include <iostream>

int main() {
    string s1 = "Hello World";
    cout << "s1 is \"Hello World\"" << endl;
    string s2 = s1;
    cout << "s2 is initialized by s1" << endl;
    string s3(s1);
    cout << "s3 is initialized by s1" << endl;
    // compare by '=='
    cout << "Compared by '==':" << endl;
    cout << "s1 and \"Hello World\": " << (s1 == "Hello World") << endl;
    cout << "s1 and s2: " << (s1 == s2) << endl;
    cout << "s1 and s3: " << (s1 == s3) << endl;
    // compare by 'compare'
    cout << "Compared by 'compare':" << endl;
    cout << "s1 and \"Hello World\": " << !s1.compare("Hello World") << endl;
    cout << "s1 and s2: " << !s1.compare(s2) << endl;
    cout << "s1 and s3: " << !s1.compare(s3) << endl;
}
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        // initialize
        String s1 = "Hello World";
        System.out.println("s1 is \"" + s1 + "\"");
        String s2 = s1;
        System.out.println("s2 is another reference to s1.");
        String s3 = new String(s1);
        System.out.println("s3 is a copy of s1.");
        // compare using '=='
        System.out.println("Compared by '==':");
        // true since string is immutable and s1 is binded to "Hello World"
        System.out.println("s1 and \"Hello World\": " + (s1 == "Hello World"));
        // true since s1 and s2 is the reference of the same object
        System.out.println("s1 and s2: " + (s1 == s2));
        // false since s3 is refered to another new object
        System.out.println("s1 and s3: " + (s1 == s3));
        // compare using 'equals'
        System.out.println("Compared by 'equals':");
        System.out.println("s1 and \"Hello World\": " + s1.equals("Hello World"));
        System.out.println("s1 and s2: " + s1.equals(s2));
        System.out.println("s1 and s3: " + s1.equals(s3));
        // compare using 'compareTo'
        System.out.println("Compared by 'compareTo':");
        System.out.println("s1 and \"Hello World\": " + (s1.compareTo("Hello World") == 0));
        System.out.println("s1 and s2: " + (s1.compareTo(s2) == 0));
        System.out.println("s1 and s3: " + (s1.compareTo(s3) == 0));
    }
}
是否可变

不可变意味着一旦字符串被初始化,你就无法改变它的内容。

  1. 在某些语言(如 C ++)中,字符串是可变的。 也就是说,你可以像在数组中那样修改字符串。
  2. 在其他一些语言(如 Java)中,字符串是不可变的。
#include <iostream>

int main() {
    string s1 = "Hello World";
    s1[5] = ',';
    cout << s1 << endl;
}
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        String s1 = "Hello World";
        s1[5] = ',';
        System.out.println(s1);
    }
}
额外操作

与数组相比,我们可以对字符串执行一些额外的操作。

#include <iostream>

int main() {
    string s1 = "Hello World";
    // 1. concatenate
    s1 += "!";
    cout << s1 << endl;
    // 2. find
    cout << "The position of first 'o' is: " << s1.find('o') << endl;
    cout << "The position of last 'o' is: " << s1.rfind('o') << endl;
    // 3. get substr
    cout << s1.substr(6, 5) << endl;
}
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        String s1 = "Hello World";
        // 1. concatenate
        s1 += "!";
        System.out.println(s1);
        // 2. find
        System.out.println("The position of first 'o' is: " + s1.indexOf('o'));
        System.out.println("The position of last 'o' is: " + s1.lastIndexOf('o'));
        // 3. get substring
        System.out.println(s1.substring(6, 11));
    }
}

我们应该了解这些内置操作的时间复杂度。

例如,如果字符串的长度是 N,那么查找操作和子字符串操作的时间复杂度是 O(N)

此外,在字符串不可变的语言中,应该额外小心连接操作。

在计算解决方案的时间复杂度时,不要忘记考虑内置操作的时间复杂度。

不可变字符串 —— 问题和解决方案

在Java中,字符串是不可变的,如果字符串是不可变的,则会带来一些问题。

修改操作

不可变字符串无法被修改。哪怕你只是想修改其中的一个字符,也必须创建一个新的字符串。

小心 Java 中的字符串

应该非常小心字符串连接

#include <iostream>

int main() {
    string s = "";
    int n = 10000;
    for (int i = 0; i < n; i++) {
        s += "hello";
    }
}
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        String s = "";
        int n = 10000;
        for (int i = 0; i < n; i++) {
            s += "hello";
        }
    }
}

在 Java 中,由于字符串是不可变的,因此在连接时首先为新字符串分配足够的空间,复制旧字符串中的内容并附加到新字符串。

因此,总时间复杂度将是:

   5 + 5 × 2 + 5 × 3 + … + 5 × n
= 5 × (1 + 2 + 3 + … + n)
= 5 × n × (n + 1) / 2,

也就是 O(n2)

在 C++ 中没有明显的性能影响。

解决方案
  1. 如果你确实希望你的字符串是可变的,则可以将其转换为字符数组。
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        String s = "Hello World";
        char[] str = s.toCharArray();
        str[5] = ',';
        System.out.println(str);
    }
}
  1. 如果你经常必须连接字符串,最好使用一些其他的数据结构,如 StringBuilder 。 以下代码以 O(n) 的复杂度运行。
// "static void main" must be defined in a public class.
public class Main {
    public static void main(String[] args) {
        int n = 10000;
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < n; i++) {
            str.append("hello");
        }
        String s = str.toString();
    }
}
经典案例
二进制求和

给定两个二进制字符串,返回他们的和(用二进制表示)。

输入为非空字符串且只包含数字 1 和 0。

示例 1:

输入: a = “11”, b = “1”
输出: “100”

示例 2:

输入: a = “1010”, b = “1011”
输出: “10101”

class Solution {
    public String addBinary(String a, String b) {
        if(a.length() < b.length()) {
            String temp = a;
            a = b;
            b = temp;
        }
        
        char[] ac = a.toCharArray();
        char[] bc = b.toCharArray();
        int alen = ac.length;
        int blen = bc.length;
        char[] cc = new char[alen + 1];
        
        char carray = '0';        
        int i = alen - 1;
        int j = blen - 1;
        while(j >= 0) {
            if(ac[i] == '0' && bc[j] == '0' && carray == '0') {
                cc[i + 1] = '0';
                carray = '0';
            }else if((ac[i] == '0' && bc[j] == '1' && carray == '0') ||
                    (ac[i] == '1' && bc[j] == '0' && carray == '0') ||
                    (ac[i] == '0' && bc[j] == '0' && carray == '1')) {
                cc[i + 1] = '1';
                carray = '0';
            } else if((ac[i] == '1' && bc[j] == '1' && carray == '1')) {
                 cc[i + 1] = '1';
                carray = '1';
            }else {
                cc[i + 1] = '0';
                carray = '1';
            }  
            i--;
            j--;
        }
        while(i >= 0) {
            if(ac[i] == '1' && carray == '1') {
                cc[i + 1] = '0';
                carray = '1';
            } else if((ac[i] == '0' && carray == '1') || (ac[i] == '1' && carray == '0')){
                cc[i + 1] = '1';
                carray = '0';
            } else {
                cc[i + 1] = '0';
                carray = '0';
            }
            i--;
        }
        cc[i + 1] = carray;
        
        return cc[0] == '1' ? String.copyValueOf(cc) : String.copyValueOf(cc, 1, alen);
            
    }
}
实现 strStr()

实现 strStr() 函数。

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

示例 1:

输入: haystack = “hello”, needle = “ll”
输出: 2

示例 2:

输入: haystack = “aaaaa”, needle = “bba”
输出: -1

说明:

needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。

class Solution {
    public int strStr(String haystack, String needle) {
        int len = needle.length();
        for(int i = 0; i <= haystack.length() - len; i++) {
            if(haystack.substring(i, i + len).equals(needle)) {
                return i;
            }
        }
        return -1;
    }
}
最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:

输入: [“flower”,“flow”,“flight”]
输出: “fl”

示例 2:

输入: [“dog”,“racecar”,“car”]
输出: “”
解释: 输入不存在公共前缀。

说明:
所有输入只包含小写字母 a-z

class Solution {
    public String longestCommonPrefix(String[] strs) {
        int len = strs.length;
		if (len == 0) {
			return "";
		}
		int len1 = strs[0].length();
		StringBuilder res = new StringBuilder();
		for (int i = 0; i < len1; i++) {
			for (int j = 1; j < len; j++) {
				if (i > strs[j].length() - 1 || strs[0].charAt(i) != strs[j].charAt(i)) {
					return res.toString();
				}
			}
			res.append(strs[0].charAt(i));

		}

		return res.toString();
    }
}

双指针技巧

情景一

通常,我们只使用从第一个元素开始并在最后一个元素结束的一个指针来进行迭代。 但是,有时候,我们可能需要同时使用两个指针来进行迭代。

看一个经典问题:

反转数组中的元素。

其思想是将第一个元素与末尾进行交换,再向前移动到下一个元素,并不断地交换,直到它到达中间位置。

我们可以同时使用两个指针来完成迭代:一个从第一个元素开始,另一个从最后一个元素开始。持续交换它们所指向的元素,直到这两个指针相遇。

void reverse(int *v, int N) {
    int i = 0;
    int j = N - 1;
    while (i < j) {
        swap(v[i], v[j]);
        i++;
        j--;
    }
}

public static void reverse(int[] v, int N) {
    int i = 0;
    int j = N - 1;
    while (i < j) {
        swap(v, i, j);  // this is a self-defined function
        i++;
        j--;
    }
}

总之,使用双指针技巧的典型场景之一是你想要

从两端向中间迭代数组。

这时你可以使用双指针技巧:

一个指针从始端开始,而另一个指针从末端开始。

值得注意的是,这种技巧经常在排序数组中使用。

经典案例
反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

示例 1:

输入:[“h”,“e”,“l”,“l”,“o”]
输出:[“o”,“l”,“l”,“e”,“h”]

示例 2:

输入:[“H”,“a”,“n”,“n”,“a”,“h”]
输出:[“h”,“a”,“n”,“n”,“a”,“H”]

class Solution {
    public void reverseString(char[] s) {
        int len = s.length;
        for(int i = 0, j = len - 1; i < j; i++, j--) {
            char temp = s[i];
            s[i] = s[j];
            s[j] = temp;
        }
    }
}
数组拆分 I

给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), …, (an, bn) ,使得从1 到 n 的 min(ai, bi) 总和最大。

示例 1:

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

输出: 4
解释: n 等于 2, 最大总和为 4 = min(1, 2) + min(3, 4).

提示:

  1. n 是正整数,范围在 [1, 10000].
  2. 数组中的元素范围在 [-10000, 10000].
class Solution {
    public int arrayPairSum(int[] nums) {
        //先排序
        insertSort(nums);
        int sum = 0;
        for(int i = 0; i < nums.length / 2; i++) {
            sum += nums[i * 2];
        }
        return sum;
    }
    public void insertSort(int[] array){
        if (array.length <= 1){
            return;
        }
        int guard;
        for (int i = 1; i < array.length ;i++){
            guard = array[i];
            if (array[i] < array[i - 1]){
                array[i] = array[i - 1];
                int j;
                for (j = i - 1;j >= 0 && array[j] > guard;j--){
                    array[j + 1] = array[j];
                }
                array[j + 1] = guard;
            }
        }
    }
}
两数之和 II - 输入有序数组

给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。

函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。

说明:

返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例:

输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        Map<Integer, Integer> map = new HashMap<>();
        int[] index = new int[2];
        for(int i = 0; i < numbers.length; i++) {
            if(map.containsKey(target - numbers[i])) {
                index[1] = i + 1;
                index[0] = map.get(target - numbers[i]);
                break;
            }
            map.put(numbers[i], i + 1);
        }
        return index;
    }
}
情景二

有时,我们可以使用两个不同步的指针来解决问题。

再看另一个经典问题:

给定一个数组和一个值,原地删除该值的所有实例并返回新的长度。

如果我们没有空间复杂度上的限制,那就更容易了。我们可以初始化一个新的数组来存储答案。如果元素不等于给定的目标值,则迭代原始数组并将元素添加到新的数组中。

实际上,它相当于使用了两个指针,一个用于原始数组的迭代,另一个总是指向新数组的最后一个位置。

现在让我们重新考虑空间受到限制的情况。

我们可以采用类似的策略,我们继续使用两个指针:一个仍然用于迭代,而第二个指针总是指向下一次添加的位置

int removeElement(vector<int>& nums, int val) {
    int k = 0;
    for (int i = 0; i < nums.size(); ++i) {
        if (nums[i] != val) {
            nums[k] = nums[i];
            ++k;
        }
    }
    return k;
}
public int removeElement(int[] nums, int val) {
    int k = 0;
    for (int i = 0; i < nums.length; ++i) {
        if (nums[i] != val) {
            nums[k] = nums[i];
            k++;
        }
    }
    return k;
}

在上面的例子中,我们使用两个指针,一个快指针 i 和一个慢指针 ki 每次移动一步,而 k 只在添加新的被需要的值时才移动一步。

这是你需要使用双指针技巧的一种非常常见的情况:

同时有一个慢指针和一个快指针。

解决这类问题的关键是

确定两个指针的移动策略。

与前一个场景类似,你有时可能需要在使用双指针技巧之前对数组进行排序,也可能需要运用贪心想法来决定你的运动策略。

经典案例
移除元素

给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

给定 nums = [3,2,2,3], val = 3,

函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 nums = [0,1,2,2,3,0,4,2], val = 2,

函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。

注意这五个元素可为任意顺序。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。 // 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
        print(nums[i]);
}

class Solution {
    public int removeElement(int[] nums, int val) {
		int count = 0;
		for(int i = 0; i < nums.length; i++) {
			if(nums[i] != val) {
				nums[count] = nums[i];
                count++;
			}
		}
		return count;
    }
}

class Solution {
    public int removeElement(int[] nums, int val) {
        int i = 0;
		int j = nums.length - 1;
		for(i = 0; i <= j; i++) {
			if(nums[i] == val) {
				while(nums[j] == val && i < j) {
					j--;
				}
                if(i == j) {
                    return i;
                }
				nums[i] = nums[j];
				j--;
			}
		}
		return i;
    }
}

最大连续1的个数

给定一个二进制数组, 计算其中最大连续1的个数。

示例 1:

输入: [1,1,0,1,1,1]
输出: 3
解释: 开头的两位和最后的三位都是连续1,所以最大连续1的个数是 3.

注意:

  • 输入的数组只包含 0 和1。
  • 输入数组的长度是正整数,且不超过 10,000。
class Solution {
    public int findMaxConsecutiveOnes(int[] nums) {
        int max = 0;
        for(int i = 0; i < nums.length; i++) {
            if(nums[i] != 1) {
                continue;
            }
            int j = i + 1;
            for(; j < nums.length; j++) {
                if(nums[j] != 1) {
                    break;
                }
            }
            max = max > j - i ? max : j - i;
        }
        return max;
    }
}
长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组。如果不存在符合条件的连续子数组,返回 0。

示例:

输入: s = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的连续子数组。

进阶:
如果你已经完成了O(n)时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int min = nums.length;
        boolean flag = true;
        for(int i = 0; i < nums.length; i++) {
            int sum = 0;
            for(int j = i; j < nums.length; j++) {
                sum += nums[j];
                if(sum >= s) {
                    min = min < j - i + 1 ? min : j - i + 1;
                    flag = false;
                    break;
                }
            }
        }    
        return flag ? 0 : min;
    }
}

小结

数组相关的技术

你可能想要了解更多与数组相关的数据结构或技术。我们不会深入研究这张卡片中的大多数概念,而是在本文中提供相应卡片的链接。

  • 这里有一些其他类似于数组的数据结构,但具有一些不同的属性:

  • 正如我们所提到的,我们可以调用内置函数来对数组进行排序。但是,理解一些广泛使用的排序算法的原理及其复杂度是很有用的。

  • 二分查找也是一种重要的技术,用于在排序数组中搜索特定的元素。

  • 我们在这一章中引入了双指针技巧。想要灵活运用该技技巧是不容易的。这一技巧也可以用来解决:

  • 双指针技巧有时与贪心算法有关,它可以帮助我们设计指针的移动策略。

经典案例
旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]

示例 2:

输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]

说明:

  • 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
  • 要求使用空间复杂度为 O(1) 的 原地 算法。
class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k = k % n;
        for(int i = 0; i < k; i++) {
            for(int j = n-1; j > 0; j--) {
                nums[j-1] = nums[j-1] ^ nums[j];
                nums[j] = nums[j] ^ nums[j-1];
                nums[j-1] = nums[j-1] ^ nums[j];
            }
        }
    }
}
杨辉三角 II

给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。

https://upload.wikimedia.org/wikipedia/commons/0/0d/PascalTriangleAnimated2.gif

在杨辉三角中,每个数是它左上方和右上方的数的和。

示例:

输入: 3
输出: [1,3,3,1]

进阶:

你可以优化你的算法到 O(k) 空间复杂度吗?

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<Integer> res = new ArrayList<>();
        for(int i = 0; i <= rowIndex; i++) {
           for(int j = i - 1; j > 0; j--) {
                res.set(j, res.get(j - 1) + res.get(j));
            }
            res.add(1);
        }
        return res;
    }
}
翻转字符串里的单词

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

示例 1:

输入: “the sky is blue”
输出: “blue is sky the”

示例 2:

输入: " hello world! "
输出: “world! hello”
解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。

示例 3:

输入: “a good example”
输出: “example good a”
解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。

说明:

  • 无空格字符构成一个单词。
  • 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。
  • 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。
class Solution {
    public String reverseWords(String s) {
        StringBuilder str = new StringBuilder();
        for(int i = s.length() - 1; i >= 0; i--) {
            if(s.charAt(i) == ' ') {
                continue;
            }
            int j = i - 1;
            for(; j >= 0; j--) {
                if(s.charAt(j) == ' ') {
                    break;
                }
            }
            str.append(s.substring(j + 1, i + 1));
            str.append(" ");
            i = j + 1;
        }
        if(str.length() == 0) {
            return "";
        }
        return str.toString().charAt(str.length() - 1) == ' ' ? str.toString().substring(0, str.length() - 1) : str.toString();
    }
}
反转字符串中的单词 III

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

示例 1:

输入: “Let’s take LeetCode contest”
输出: “s’teL ekat edoCteeL tsetnoc”

注意:在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。

class Solution {
    public String reverseWords(String s) {
        StringBuilder str = new StringBuilder();
        for(int i = 0; i < s.length(); i++) {
            int j = i + 1;
            for(; j < s.length(); j++) {
                if(s.charAt(j) == ' ') {
                    break;
                }
            }
            int len = j - i;
            while(j > i) {
                j--;
                str.append(s.charAt(j));
            }
            str.append(" ");
            i = i + len;
        }
        return str.toString().trim();
    }
}
删除排序数组中的重复项

给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。

不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。

示例 1:

给定数组 nums = [1,1,2],

函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。

你不需要考虑数组中超出新长度后面的元素。

示例 2:

给定 nums = [0,0,1,1,1,2,2,3,3,4],

函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。

你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以“引用”方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
        print(nums[i]);
}

class Solution {
    public int removeDuplicates(int[] nums) {
        
    }
}
//双指针法,快慢指针
func removeDuplicates(nums []int) int {
    l := len(nums)
    //ptr := &nums
    i := 0
    for j := 1; j < l; j++ {
        if(nums[i] != nums[j]) {
            i++
            nums[i] = nums[j]
        }    
    }
    return i + 1
}
移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

示例:

输入: [0,1,0,3,12]
输出: [1,3,12,0,0]

说明:

  1. 必须在原数组上操作,不能拷贝额外的数组。
  2. 尽量减少操作次数。
class Solution {
    public void moveZeroes(int[] nums) {
        
    }
}
func moveZeroes(nums []int)  {
    i := 0
    for j := 0; j < len(nums); j++ {
        if nums[j] != 0 {
            nums[i] = nums[j]
            i++
        }
    }
    for k := i; k < len(nums); k++ {
        nums[k] = 0
    } 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值