编程算法思想小结

所谓编程算法思想,就是看到一道编程题后你有什么与此题有关的算法思想。
以下我分4种思想并结合具体例题来分别进行阐述。
1.0,1思想:在某些编程问题需要表示事物两种不同的状态,这时候就需要用到0,1思想。
例1:有n个人围成一圈,顺序排号。从第一个人开始报数(从1到3报数),凡报到3的人退出圈子,问最后留下的是原来第几号的那位?
这个题中显然有在场和离场两种状态,此时就可以用1表示在场(数组初始化为1),
用0表示离场表示被淘汰离场(按条件每计数到3的数组就赋值为0)。有了这个思想作为此题的突破口,具体代码也就迎刃而解了。
下面给出此题与此相关的部分核心代码:

for (int i = 0; i < n; i++) {
            arr[i] = 1;
        }
        int num = n;
        int count = 0;
        while (num > 1) {
            for (int i = 0; i < n; i++) {
                if (arr[i] == 1) {
                    count++;
                }
                if (count == 3) {
                    arr[i] = 0;
                    num--;
                    count = 0;
                }
            }
        }

例2:(不同路径问题)一个机器人位于一个 m x n 网格的左上角 。机器人每次只能向下或者向右移动一步。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1 和 0 来表示。
此题题目就已经用0,1分别表示空位置和障碍物的两种状态了,然后再结合简单动态规划思想,此题也便不难求解。
下面给出此题与此相关的部分核心代码:

for(int i = 0; i<obstacleGrid.length;i++){
            for(int j = 0 ; j<obstacleGrid[i].length;j++){
                if(obstacleGrid[i][j] == 1){//操作障碍点(包括边缘的障碍点)
                    obstacleGrid[i][j] = 0;
                    continue;//很重要,使得一个点只会被判断进行一次操作
                }
                if(i == 0 && j == 0){//操作起点
                    obstacleGrid[i][j] = 1;
                    continue;
                }
                if(i == 0 || j == 0){//操作边缘点(如果遇到有障碍点,则后面的点都为0)
                    obstacleGrid[i][j] = i == 0?obstacleGrid[i][j-1]:obstacleGrid[i-1][j];
                    continue;
                }
                obstacleGrid[i][j] = obstacleGrid[i-1][j]+obstacleGrid[i][j-1];//操作正常点
            }
        }

此外,0,1思想还有一个重要的用途,在某些遍历上。例如图,为了避免重复遍历,先把所有的节点初始化为0,每遍历一个就赋值为1。

2.双/三指针思想:在某些编程问题需要用多个变量来同时表示一个事物,这时候就需要用到双/三指针思想。
例1:给定一个数组 nums 和一个值 val,你需要原地移除所有数值等于 val 的元素,返回移除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
由于只能在原数组上进行修改,但是又要遍历原数组,所有此时一个指针变量以及满足不了需求了,便使用双指针来解决此问题。
下面给出此题与此相关的部分核心代码:

for (int j = 0; j < nums.length; j++) {
            if (nums[j] != val) {
                nums[i] = nums[j];
                i++;
            }
        }

例2:编写一个程序,找出第 n 个丑数。丑数就是只包含质因数 2, 3, 5 的正整数。
此题需要再创建一个数组来存储丑数,但是由于丑数有三个约束条件,所以此时需要创建三个指针来分别指向2的倍数,3的倍数和5的倍数,有了这三个指针后面的编程也就变得简单了许多。
下面给出此题与此相关的部分核心代码:

for (int i = 1;i < n;i++) {
            int multiply2 = result[divisor2] * 2;
            int multiply3 = result[divisor3] * 3;
            int multiply5 = result[divisor5] * 5;
            int min = Math.min(multiply2,Math.min(multiply3,multiply5));
            result[i] = min;
            if (multiply2 == min) {
                divisor2++;
            }
            if (multiply3 == min) {
                divisor3++;
            }
            if (multiply5 == min) {
                divisor5++;
            }
        }

例3:给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
这题也需要创建三个指针,一个用于表示遍历数组的指针,一个用于顺方向的比较的指针,另一个用于逆方向比较的指针,有了这三个指针后面的编程也就变得简单了许多。
下面给出此题与此相关的部分核心代码:

 for (int i = 0; i < nums.length - 2; i++) {
            if (i == 0 || (nums[i] != nums[i - 1])) {  // 跳过重复的nums[i]
                int l = i + 1, r = nums.length - 1, sum = 0 - nums[i];
                while (l < r) {
                    if (nums[l] + nums[r] == sum) {
                        ls.add(Arrays.asList(nums[i], nums[l], nums[r]));
                        while (l < r && nums[l] == nums[l + 1]) l++;// 跳过重复的nums[l]
                        while (l < r && nums[r] == nums[r - 1]) r--;// 跳过重复的nums[r]
                        l++;
                        r--;
                    } else if (nums[l] + nums[r] < sum) {
                        while (l < r && nums[l] == nums[l + 1]) l++;// 跳过重复的nums[l]
                        l++;
                    } else {
                        while (l < r && nums[r] == nums[r - 1]) r--;// 跳过重复的nums[r]
                        r--;
                    }
                }
            }
        }

此外,在链表等许多地方也用到了多指针的思想,例如反转单链表的两种方法就1都需要用到三指针的思想。

3.栈(递归)/队列思想:顾名思义,此类思想需要用到栈(先进后出),队列(先进先出)的思想来解决问题。
例1:给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效。
由于括号是就近的匹配,所以此题需要用到栈来解决。可以先将字符串的左括号全都放入一个栈中,然后再依次取出和右括号进行匹配,此题也便迎刃而解了。
下面给出此题与此相关的部分核心代码:

if (s.charAt(i)=='(' || s.charAt(i)=='{' || s.charAt(i)=='[') {
                    stack[top++] = s.charAt(i);
                    }
                 else {
                    if(top == 0) {
                        return false;
                    }
                    char c = stack[top-1];
                    if ('(' == c && ')' == s.charAt(i) ||
                            '{' == c && '}' == s.charAt(i) ||
                            '[' == c && ']' == s.charAt(i)) {
                        --top;
                    }
                    else {
                        return false;
                    }
                }

例2:给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于n/2的元素。
此题不难,一个简单的思路就是对原数组先排序,然后返回数组中间的那个元素。
但是通过栈可以巧妙的避免排序所导致的时间复杂度过大。由于众数的数量大于数组元素的一半,所以可以将原数组元素依次进行入栈。入栈的规则为,栈空直接入,如果数组元素与栈顶元素一样,直接入栈,如果不一样数组元素不入栈,栈顶元素出栈。这样一来最后栈顶元素一定就是众数。
下面给出此题与此相关的部分核心代码:

for (int i = 0; i < nums.length; i++) {
            if (top == 0 || stack[top-1] == nums[i]) {
                stack[top] = nums[i];
                top++;
            }
            else {
                top--;
            }
        }

例3:给定字符串 s 和 t ,判断 s 是否为 t 的子序列。你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
此题的一个巧妙方法是可以用队列来实现,先把两字符串都变为数组,然后把短的字符串放入一个队列,然后遍历长数组,并且每次都和对顶元素进行比较,如果一样,对顶元素出队列,最后如果队列为空那么s就是t的子序列。
下面给出此题与此相关的部分核心代码:

for (int i = 0; i < queue2.length; i++) {
            if (top != queue1.length-1 && queue2[i] == queue1[top]) {
                top++;
                continue;
            }
            if (top == queue1.length-1 && queue2[i] == queue1[top]) {
                top++;
                break;
            }
        }

此外,栈和队列作为两种及其重要的数据结构,它们的使用场景还有很多地方,例如栈的先进后出的思想和递归算法也是极其相似,因为递归本身就是最先进行递归的方法最后再返回。

4.重构数组思想:表面上只是重新再创建一个数组,但是这个可不单单是一个普通的数组,由于我的见识面尚浅,所以暂时只能以计数排序算法思想和归并排序算法思想为初始点进行展开。
在计数排序中,我们新创建了两个数组,第一个数组用来存储原数组各值的元素数目,进而可以变形为后面元素表示前面元素之和。第二个数组用于存储有序的原数组元素,但是这个有序并没有那么简单,第二个数组的元素下标和第一个数组元素有关,而第一个数组的元素下标又与原数组元素有关,这也便是此重构数组的特殊之处。
下面给出计数排序与此相关的部分核心代码:

        //2.创建统计数组并统计对应元素个数
        int[] countArray = new int[d+1];
        for (int i = 0; i < array.length; i++) {
            countArray[array[i]-min]++;
        }
        //3.统计数组做变形,后面元素等于前面的元素之和
        int sum = 0;
        for (int i = 0; i < countArray.length; i++) {
            sum += countArray[i];
            countArray[i] = sum;
        }
        //4.倒序遍历原始数组,从统计数组找到正确位置,输出结果数组
        int[] sortedArray = new int[array.length];
        for (int i = array.length-1; i >= 0; i--) {
            sortedArray[countArray[array[i]-min]-1] = array[i];//-1是因为sortedArray数组也是从0号下标开始计数
            countArray[array[i]-min]--;
        }

例1:给定一个英文字符串,写一段代码找出字符串中首先出现三次的英文字母。
这题并不难,但也用到了上述的重构数组的思想。首先我们得把此字符串转变为字符数组,然后我们需要遍历数组来找到最先出现三次的那个英文字母。此时我们就需要新创建一个数组来用于计数每一种字母出现的次数,而这个新数组的下标恰恰就是原数组中的字母元素。
下面给出此题与此相关的部分核心代码:

for (int i = 0; i < ch.length; i++) {
            char p = ch[i];
            if (p >= 'a' && p <= 'z' || p >= 'A' && p <= 'Z') {
                hash[p]++;
            }
            if (hash[p] == 3) {
                return p;
            }
        }

例2:给定一个未排序的整数数组,找出其中没有出现的最小的正整数。你的算法的时间复杂度应为O(n)。
这个题有一个很巧妙的方法,那便是重构数组。我们先创建一个原数组长度的新数组,然后把原数组中大于1并且小于或等于原数组长度的元素赋值给新的数组,而新数组的下标恰恰就是原数组元素大小-1,此时我们得到的新数组便是一个有序的正数数组(不考虑新数组后面初始化为0的元素)。
下面给出此题与此相关的部分核心代码:

 for (int j = 0; j < nums.length; j++) {
            if (nums[j] >= 1 && nums[j] <= nums.length) {
                array[nums[j]-1] = nums[j];//让新的array数组形成了以下标+1为基准的排序
            }
        }

在归并排序的流程中,我们需要把两段有序的数组合并成一段有序的数组,此时我们这段新的数组就是我们所创建的新数组,而它的独特之处在于它在被完全赋值之后就已经是一个有序的数组。
下面给出归并排序与此相关的部分核心代码:

 while(start <= mid && start2 <= end) {
            if(array[start] <= array[start2]) {
                tmpArray[tmpIndex++] = array[start++];
            }else {
                tmpArray[tmpIndex++] = array[start2++];
            }
        }
        while (start <= mid) {
            tmpArray[tmpIndex++] = array[start++];
        }
        while (start2 <= end) {
            tmpArray[tmpIndex++] = array[start2++];
        }

例3:给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
此题完全可以借用上面归并排序算法的思想,创建出一个有序的包含此两原数组元素的新数组,然后返回其中间值即可。
下面给出此题与此相关的部分核心代码:

while (i <= nums1.length-1 && j <= nums2.length-1) {
           if (nums1[i] <= nums2[j]) {
                array[k++] = nums1[i++];
            }
            else{
                array[k++] = nums2[j++];
            }
        }
        while (i <= nums1.length-1) {
            array[k++] = nums1[i++];
        }
        while (j <= nums2.length-1) {
            array[k++] = nums2[j++];
        }

此外,其实上面的栈和队列的思想很多时候也是一种重构数组思想,因为此时我们新创建的栈和队列也是一个数组。
最后,由于我接触的题目实在有限,所以暂时也只能想到这么几条编程算法思想以及其拓展,但是随着自己的深入学习,我会相信我还会发现更多的编程算法思想及其拓展。学习贵在坚持和探索!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章