《算法题讲解指南:优选算法-双指针》--01移动零,02复写零

《算法题讲解指南:优选算法-双指针》--01移动零,02复写零

🔥小叶-duck个人主页

❄️个人专栏《Data-Structure-Learning》

《C++入门到进阶&自我学习过程记录》

未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游


目录

一、双指针算法介绍

  1、对撞指针

  2、快慢指针

01、移动零

题目链接:

题目描述:

题目示例:

算法思路:

算法流程:

C++ 代码演示:

算法总结:

02、复写零

题目链接:

题目描述:

题目示例:

算法思路:

算法流程:

C++代码演示:

算法总结及流程解析:

结束语


一、双指针算法介绍

      在正式讲解本次的算法题之前我们先来看看算法中一个非常常用的方法——双指针。双指针有两种形式,一种对撞指针,一种是左右指针。

  1、对撞指针

一般用于顺序结构中,也称左右指针。

  • 对撞指针从两端向中间移动。一个指针从最左端开始,另一个从最右端开始,然后逐渐往中间逼近。
  • 对撞指针的终止条件一般是两个指针相遇或者错开(也可能在循环内部找到结构直接跳出循环),也就是:left == right(两个指针指向同一个位置),left > right(两个指针错开)

  2、快慢指针

又称龟兔赛跑算法,其基本思想就是使用两个移动速度不同的指针在数组或链表等序列结构上移动。

      这种方法对于处理环形链表或数组非常有用。其实不单单是环形链表或者数组,如果我们要研究的问题出现循环往复的情况时,均可考虑使用快慢指针的思想。

快慢指针的实现方法有很多种,最常用的一种就是:

  • 在一次循环中,每次让慢的指针向后移动一位,而快的指针往后移动两位,实现一快一慢

01、移动零

题目链接:

283. 移动零 - 力扣(LeetCode)

题目描述:

题目示例:

解法:(快排的思想:数组划分区间-数组分块)

      【数组分块】是非常常见的一种算法技巧,主要就是根据一种划分方式,将数组的内容分成左右两部分。这种类型的题,一般就是使用【双指针】来解决。

算法思路:
  • 我们可以用一个 cur 指针来扫描整个数组,另一个 dest 指针用来记录非零序列的最后一个位置,根据 cur 在扫描的过程中,遇到的不同情况,分类处理,实现数组的划分。
  • 在 cur 遍历期间,保证【0,dest】区间的元素全部都是非零元素,【dest+1,cur-1】区间的元素全是零,而 cur 后面的元素则是未处理的。
算法流程:

  1、初始化 cur = 0(用来遍历数组),dest = -1(指向非零元素序列的最后一个位置。因为刚开始还没有进行遍历,此时相当于还没有非零元素的序列,因此初始化为 -1

  2.cur 依次往后遍历每个元素,遍历到的元素会有下面两种情况:
    2.1 遇到的元素是 0 ,cur直接++ ,不需要对 dest 进行操作。因为我们的目标是让【dest+1,cur-1】内的元素全都是0,因此当 cur  遇到 0 的时候,直接 ++ ,就可以保证【dest+1,cur-1】这个区间内依然全为0;
    2.2 遇到的元素不是 dest++,并且交换 cur 位置和 dest 位置的元素,之后让 cur++,扫描下一个元素。

  • 因为 dest 指向的位置是非零元素区间的最后一个位置,如果扫描到一个新的非零元素,那么这个非零元素的位置应该在 dest+1 的位置上,因此 dest 先自增 1
  • dest++ 之后,指向的元素就是 元素(因为非零元素区间末尾的后一个元素(下标为 dest+1)就是 ),因此可以交换到 cur 所处的位置上,实现【0,dest】的元素全部都是非零元素,【dest+1,cur-1】的元素全是零。

C++ 代码演示:

class Solution { public: void moveZeroes(vector<int>& nums) { int dest = -1; int cur = 0; while(cur != nums.size()) { // if(nums[cur] == 0) // { // cur++; // } // else // { // swap(nums[++dest], nums[cur++]); // } //代码优化 if(nums[cur])//if判断为假则说明cur遇到0 { swap(nums[++dest], nums[cur]); } cur++; } } };

算法总结:

      这里用到的方法就是我们在数据结构中学习 【快排算法】的时候,【数据划分】过程的重要一步。如果将快排算法的递归拆解成单趟的话,这一小段代码就是实现快排单趟的【核心步骤】。

02、复写零

题目链接:

1089. 复写零 - 力扣(LeetCode)

题目描述:

题目示例:

解法:(原地复写——双指针)

算法思路:
  • 如果【从前往后】进行原地复写操作的话,由于 0 的出现会复写两次,导致没有复写的数【被覆盖掉】。因此我们选择【从后往前】的复写策略。
  • 但是 【从后往前】复写的时候,我们需要找到 【最后一个复写的数】,因此我们的大体流程分两步:1.先找到最后一个复写的数;2.然后从后往前进行复写操作
算法流程:

  1.初始化两个指针 cur = 0dest = -1

  2.找到最后一个复写的数:

  • 判断 cur 位置的元素:(1)如果是 0 的话,dest 往后移动两位;(2)否则,dest 往后移动一位
  • 判断 dest 这时候是否已经到结束位置,如果结束就终止循环;
  • 如果没有结束,cur++,继续判断。

  3.判断 dest 是否越界到 的位置:

如果越界,执行下面三步:

  • n-1 位置的值修改成 0 ;
  • cur 向前移动一步(cur--)
  • dest 向前移动两步(dest -= 2);

  4.从 cur 位置开始往前遍历原数组,依次还原出复写后的结果数组:

    4.1 判断 cur 位置的值:

  • 如果是 0 :dest 以及 dest-1 位置修改成 dest-=2
  • 如果非零:dest 位置修改成 0 ,dest -= 1

    4.2 cur--,复写下一个位置。

C++代码演示:

class Solution { public: void duplicateZeros(vector<int>& arr) { // 1. 先找到最后⼀个复写的数 int dest = -1; int cur = 0; while(1) { dest++; if(arr[cur] == 0) { dest++; } if(dest >= arr.size() - 1) { break; } cur++; } // 2. 处理越界情况 if(dest == arr.size()) { arr[arr.size() - 1] = 0; dest -= 2; cur--; }//之所以会越界是因为如果最后一个复写的数为0 //则可能出现复写的位置在下标n-1和n,但下标为n就是越界 //也就是说实际上并没有复写两遍0而只是数组最后一个位置复写为0 //所以如果越界操作就是数组最后位置手动置为0后让dest回到倒数第二个位置 //cur--就是让最后一个复写的数变成前一个 // 3. 从后向前完成复写操作 while(cur >= 0) { if(arr[cur] == 0) { arr[dest--] = arr[cur]; } arr[dest--] = arr[cur--]; } } };

算法总结及流程解析:

结束语

      到此,本次的算法题就讲解完了,这是小萌新讲解算法题的开始,后面我会不断地对非常经典且精选的算法题为大家进行讲解,希望大家能有所收获!

Read more

C++红黑树的深入解析:从理论到实践

C++红黑树的深入解析:从理论到实践

红黑树作为一种自平衡的二叉搜索树,是计算机科学中的经典数据结构之一,广泛应用于各种需要高效查找、插入和删除操作的场景中,比如STL中的 map 和 set。虽然它的基本原理并不复杂,但在实现过程中,需要处理许多细节,比如颜色的调整、旋转操作等。本文将对红黑树的结构、插入、删除等操作进行详细的剖析,以帮助大家更好地理解和实现这一高效的数据结构。 一、红黑树的基本概念与规则 红黑树是一种满足特定性质的二叉搜索树。与普通的二叉搜索树不同,红黑树的每个结点都附加了一个颜色属性,且通过一些规则保证了树的平衡性。红黑树的四条基本规则如下: 每个节点的颜色要么是红色,要么是黑色。根节点是黑色的。如果一个节点是红色的,那么它的两个子节点必须是黑色的。 这就意味着红色节点不能有连续的红色节点。从任意一个节点到其所有叶子节点的路径上,必须包含相同数量的黑色节点。 这条规则保证了树的平衡性,防止树的某些路径过长。 红黑树的这些规则通过“颜色约束”来间接实现自平衡,因此每次进行插入、删除操作时,都需要确保这些规则被满足。 以上均为红黑树示例。 二、红黑树的高度与效率 红黑树的高度是它的关键特性之

By Ne0inhk
【C++】 —— 笔试刷题day_29

【C++】 —— 笔试刷题day_29

一、排序子序列 题目解析 一个数组的连续子序列,如果这个子序列是非递增或者非递减的;这个连续的子序列就是排序子序列。 现在给定一个数组,然后然我们判断这个子序列可以划分成多少个排序子序列。 例如:1 2 3 2 2 1 可以划分成 1 2 3和2 2 1两个排序子序列 算法思路 对于这道题,思路就很简单了: 暴力解法 从0位置开始遍历,寻找一段连续子序列,直到这一段子序列不满足排序子序列的条件,即找到了一个排序子序列; 然后继续从上次遍历结束位置接着遍历数组,寻找下一个子序列。 贪心优化: 当我们遍历到数组中数值相同的区域时,我们要知道,要找的子序列是非递增或者非递减的; 所以这一段相等的序列,我们可以加到前面或者后面的任意序列中。 所以我们在遇到数值相等的子序列时,继续向后遍历即可。 但是这样我们会发现两个问题: 如果数组刚开始位置的数据是相等的,我们不知道这段子序列是非递增的还是非递减的;我们在遍历过程中会用到下一个位置的值来判断是非递增还是非递减,那最后一个位置该怎么办? 对于数组开头的相等子序列,我们可以直接跳过,因为这一段相等的序列可以加到

By Ne0inhk
C++ string 全面指南

C++ string 全面指南

一、模板 1. 函数模板 什么是模板呢?模板就是一个模具,只需要往这个模具里倒入不同的材料,就可以获得不同材料的铸件。 如果我们要实现一个交换函数呢?这是很容易的事情。 但是这种交换函数只能实现整型之间的交换,如果我想进行浮点数交换呢,字符型交换呢?是不是就不可以了。 虽然我们可以通过函数重载实现不同的交换函数,但是这样做太浪费时间了,没有意义。毕竟只是改变了交换函数参数的类型,代码不需要变化。所以,这种方法是有缺陷的。 1.代码复用率低。 2.可维护性差。 所以,有了函数模板,这是实现泛型编程的基础。 所谓泛型编程就是编写与类型无关的通用代码,是代码复用的一种手段。 template<typename T>就是定义了一个模板,通过一份代码就可以实现多个要求。 这里的typename也可以换成class,这两个的区别会在后面讲解。 这个就叫做函数模板,函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本。 函数模板的格式:template<typename T1, typename

By Ne0inhk
C++ string 类详解:概念、常用操作与实践(算法竞赛类)

C++ string 类详解:概念、常用操作与实践(算法竞赛类)

🔥个人主页:星轨初途 ❄专栏传送门:C语言,数据结构,C++学习(竞赛类)算法及编程题分享 文章目录 * 前言 * 一、string概念 * 二、string的常见操作和功能 * 1、头文件 * 2、创建字符串 * 3、string字符串的输入 * (1)正常输入(cin) * (2)getline(带空格输入) * 第一种(默认以‘\n’为结束标志) * 第二种(自定义结束标志) * 4、size()——字符串长度 * 5、迭代器(iterator) * begin()和end() * (1)比较 * (2)遍历 * 改变指定字符 * 6、字符串的插入和删除 * (1)插入

By Ne0inhk