双指针:移除元素、反转字符串、替换空格、翻转字符里的单词、翻转链表、删除链表的倒数第N个节点、链表相交、环形链表、三数之和(较难)、四数之和

移除元素

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

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。

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

class Solution {
public:
int removeElement(vector<int>& nums, int val) {
//使用左右指针,如果左指针为val才与右指针交换,两指针移动,如果不等则只移动左指针
	int left = 0, right = nums.size() - 1;
	while (left <= right)
	{
		if (nums[left] == val && nums[right]!=val)
		{
			swap(nums[left], nums[right]);
            nums.resize(nums.size()-1);
			right--;
            left++;
		}
        else if (nums[left] == val && nums[right] == val)
		{
			right--;
            nums.resize(nums.size()-1);
		}
        else{
			left++;
        }
	}
	return nums.size();
}
};

采用双指针的方法,从左至右逼近容器数组,两种指针分为三种情况:
1)如果两者都为val,则右指针前移一位,左指针不动;
2)如果左指针为val,右不是,则交换两者元素位置,再分别前移后移一位;
3)如果左右都不是val,则左指针前移,右指针不动
该算法时间复杂度为O(n)

例题26:给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。nums已按升序排列

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
	//同时开始快慢指针,如果快指针与慢指针相同,则找到不相同的位置从后往前覆盖
	int low = 0,fast=1,i;
	while (low < nums.size()-1)
	{
        	while (nums[fast] == nums[low] )
		{
            if(fast==nums.size()-1)
            {
            nums.resize(nums.size()-(fast-low));
            return nums.size();
            }
            else
			fast++;
		}
        int count=fast-low;
		if ( count>1)//不相同有重复
		{
			i = fast;
			while (i < nums.size())//覆盖
			{
				nums[i - (count - 1)] = nums[i];
				i++;
			}
			nums.resize(nums.size() - (count- 1));
			low++;
			fast = low + 1;
		}
		else if(count == 1)//不相同无重复
		{
			low++;
			fast = low + 1;
		}
	}
    return nums.size();
    }
};

该方法快指针找到新种类元素时,快指针倒退,增加了比较次数,可以进行改进如下:
使用快慢指针,慢指针用来存放当前元素的种类与位置,快指针遍历容器查找下一个种类,当找到第一个不同种类时就替换为慢指针的后一位;
当快指针遍历完就结束循环

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

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

void moveZores(vector<int>& nums)
{
int i = 0;
	int count = nums.size();
	while (count>0)
	{
		if (nums[i] == 0)
		{
			int low = i ;
			int fast = i + 1;
			while (fast < nums.size())
			{
				nums[low] = nums[fast];
				low++;
				fast++;
			}
			nums[nums.size() - 1] = 0;
		}
        else{
            i++;
        }
		count--;
	}
    }

在一层循环中嵌套快慢指针,如果找到元素为0,那么将后面的元素依次迁移,再将最后一个元素置零;

例题844:给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。

注意:如果对空文本输入退格字符,文本继续为空。

class Solution {
public:
string comparebackspace(string s)//得到退格后的字符串函数
{
int low=0,fast=1;
while(low<s.size())
{
    if(s[low]=='#' && low==0)//当第一位为退回符时,字符串整体前移一位,字符串长度减一
    {
     while(fast<s.size())
     {
         s[fast-1]=s[fast];
         fast++;
     }
     fast=1;
     s.resize(s.size()-1);
    }
   else if(s[low]=='#' && low!=0)//当中间位为退回符时,从后往前覆盖退回符和其前一位,字符串长度减二
    {
    while(fast<s.size())
    {
s[fast-2]=s[fast];
fast++;
    }
    low=low-1;
    fast=low+1;
    s.resize(s.size()-2);
    }
    else if(s[low]!='#')
    {
low++;
fast++;
    }
}
return s;
}

    bool backspaceCompare(string s, string t) {
string ress,rest;
ress=comparebackspace(s);
rest=comparebackspace(t);
if(ress==rest)
{
    return true;
}
else {
return false;
}
    }
};

使用的方法是构造一个函数得到退格后的字符串,再比较两字符串退格后是否相等
得到退格字符串的方法是:用双指针遍历字符串,如果慢指针为退格符,则需删去退格符及其前一字符,也就是将字符串从后往前移动两位

  • 该方法可以进行简化:使用栈处理遍历过程,每次遍历一个字符:
  • 如果该字符不是’#',则压入栈;
  • 否则,将栈顶弹出。

简化具体代码如下:

class Solution {
public:
    bool backspaceCompare(string S, string T) {
        return build(S) == build(T);
    }

    string build(string str) {
        string ret;
        for (char ch : str) {
            if (ch != '#') {
                ret.push_back(ch);
            } else if (!ret.empty()) {
                ret.pop_back();
            }
        }
        return ret;
    }
};

例题977:给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
//先平方后排序,时间复杂度最低为快排的O(nlgn)
	//用双指针指向一头一尾,将绝对值更大元素的平方插入新数组的头部,然后移动该数的指针,另一个不动,两端逼近为止
	vector<int> res;
	int left = 0, right = nums.size() - 1;
	while (left <= right)
	{
		int max;
		max = abs(nums[left]) >= abs(nums[right]) ? left : right;
		res.insert(res.begin(), nums[max]*nums[max]);//头插法res.push_front();
		if (max == left)
		{
			left++;
		}
		else if(max==right)
		{
			right--;
		}
	}
	return res;
    }
};

反转字符串

例题344:编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

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

示例

void reverseString(vector<char>& s) {
	//头尾指针向中间逼近交换元素
	int left = 0, right = s.size() - 1;
	while (left <= right)
	{
		swap(s[left], s[right]);
		left++;
		right--;
	}
}

替换空格

例题剑指offer 05题:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

string replaceSpace1(string s) {
	//开新空间:①遍历字符串,碰到空格在新字符串后加上%20,否则填入该元素本身
	 //         ②利用栈解决:从后往前将元素推入栈,如果碰到空格则推入%20,否则推入原字符
	//          ③先找到空格个数,将原字符串扩充到替换空格后的大小,碰到空格的话,将字符串从该位置往后移2位,再填入%20
	stack<char> sta;
	for (int i=s.size()-1;i>=0;i--)
	{
		if (s[i] == ' ')
		{
			sta.push('0');
			sta.push('2');
			sta.push('%');
		}
		else
		{
			sta.push(s[i]);
		}
	}
	string res;
	while (!sta.empty())
	{
		char t = sta.top();
		res.push_back(t);
		sta.pop();
	}
	return res;
}

翻转字符里的单词

例题151:给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
示例

class Solution {
public:
    string reverseWords(string s) {
        //重开空间的做法
        if(s.size()==1)
        {
            return s;
        }
    	string res;
	int right = s.size() - 1;
	int left = right;
	int t;
	while(left>0)
	{
		while (right>0 && s[right] == ' ' )
		{
			right--;
            if(right==0 && s[right]==' ')
            {
                res.erase(res.size()-1);
                return res;
            }
		}
		left = right;
		while (left>0 && s[left - 1] != ' ')
		{
			left--;
		}
		t = left-1;
		while (t < right)
		{
			res.push_back(s[++t]);
		}
			res.push_back(' ');
		right = left-1;
	}
    while(res[res.size()-1]==' ')
    {
        res.resize(res.size()-1);
    }
	return res;
    }
};

重开空间做法:从后往前依次找到每个单词将其翻转后存入新字符串,在每个单词后加一个空格,最后删去末尾的空格

翻转链表

例题206:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例

  ListNode* prehead = nullptr;
	ListNode* pre = prehead;
	ListNode* cur=head;
	ListNode* behind;
	while (cur != nullptr)
	{
		behind =cur->next;//behind会超出链表到nullptr
		cur->next = pre;
		pre = cur;
		cur = behind;
	}
	return pre;
    }
};

交换两节点位置时顺序不能换,否则cur先后移一位,pre也到了cur后移后的位置
还可以使用虚结点的头插法将原链表的节点从头至尾插入,实现链表翻转
使用栈:先将链表入栈,然后用虚结点依次连接出栈的元素

// 迭代方法:增加虚头结点,使用头插法实现链表翻转
public static ListNode reverseList1(ListNode head) {
    // 创建虚头结点
    ListNode dumpyHead = new ListNode(-1);
    dumpyHead.next = null;
    // 遍历所有节点
    ListNode cur = head;
    while(cur != null){
        ListNode temp = cur.next;
        // 头插法
        cur.next = dumpyHead.next;
        dumpyHead.next = cur;
        cur = temp;
    }
    return dumpyHead.next;
}

使用栈实现链表翻转

public ListNode reverseList(ListNode head) {
    // 如果链表为空,则返回空
    if (head == null) return null;
    // 如果链表中只有只有一个元素,则直接返回
    if (head.next == null) return head;
    // 创建栈 每一个结点都入栈
    Stack<ListNode> stack = new Stack<>();
    ListNode cur = head;
    while (cur != null) {
        stack.push(cur);
        cur = cur.next;
    }
    // 创建一个虚拟头结点
    ListNode pHead = new ListNode(0);
    cur = pHead;
    while (!stack.isEmpty()) {
        ListNode node = stack.pop();
        cur.next = node;
        cur = cur.next;
    }
    // 最后一个元素的next要赋值为空
    cur.next = null;
    return pHead.next;
}

删除链表的倒数第N个节点

例题19:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
//删除的倒数第n个,也就是正数第sz-n+1个
ListNode* dummyHead= new ListNode();
	dummyHead->next = head;
	ListNode* left = dummyHead;
	ListNode* right = head;
	int sz = 0;
	while (right != nullptr)//找到链表的节点数
	{
		sz++;
		right = right->next;
	}
	right = head;
	while (sz!=n)//找到要删除的倒数第n个节点,此时left指向其前一个,right指向倒数第n个
	{
		left = left->next;
		right = right->next;
		sz--;
	}
	left->next = right->next;
	delete right;
	return dummyHead->next;
    }
};

进阶版:使用一次遍历
定义快慢指针指向虚头节点,然后让fast先走n+1步,这时再让slow和fast同步走,当fast走到nullptr时,slow走到了要删除节点的前一位,直接删除倒数第n个节点即可
为什么要走n+1步?只有这样同时移动时slow才能指向要删除节点的前一位,方便做删除操作

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete nth;
        
        return dummyHead->next;
    }
};

链表相交

例题160:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:
示例

//算出两链表的长度差,让长链表指向头节点的指针先走差这么多位,然后两指针同时走,直到找到第一个地址相同的点为相交点
class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
	ListNode* dummyHeadA = new ListNode(-1);
	dummyHeadA->next = headA;
	ListNode* dummyHeadB = new ListNode(-1);
	dummyHeadB->next = headB;
	ListNode* p = dummyHeadA;
	ListNode* q = dummyHeadB;
	int lengthA = -1;
	int lengthB = -1;
	while (q != nullptr)
	{
		lengthB++;
        q=q->next;
	}
	while (p != nullptr)
	{
		lengthA++;
        p=p->next;
	}
	p = dummyHeadA->next;
	q = dummyHeadB->next;
	int dis = (lengthA - lengthB) >= 0 ? (lengthA - lengthB) : (lengthB - lengthA);
	if (lengthA >= lengthB)
	{
		while (dis > 0)
		{
			p = p->next;
            dis--;
		}
	}
	else
	{
		while (dis > 0)
		{
			q = q->next;
            dis--;
		}
	}
	while (q != nullptr && p != nullptr)
	{
        if (q == p)
		{
			return q;
		}
		q = q->next;
		p = p->next;
	}
	return nullptr;
    }
};

该方法时间复杂度为O(n+m),空间复杂度为O(1)

环形链表

例题142:给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。
示例

ListNode *detectCycle(ListNode *head) {
   unordered_map<ListNode*, int> hmap;
	ListNode* dummyHead = new ListNode();
	dummyHead->next = head;
	ListNode* p = dummyHead;
	while (p != nullptr)
	{
		hmap[p]++;
		if (hmap[p] > 1)
		{
			return p;
		}
        p = p->next;
	}
	return nullptr;
    }
};

该方法的进阶版:使用空间复杂度为O(1)的方法
具体实现:1.判断是否有环:使用快慢指针,快指针每次走两步,慢指针每次走一步,如果两者相遇,则一定是在环内相遇,该点为相遇点(为什么快走2,慢走1两者一定会相遇?)
2.有环后怎么找到环入口:列方程,当相遇时,慢指针一共走了x+y步,快指针一共走了x+y+n(y+z)步,而快又是慢的两倍,所以
2(x+y)=x+y+n(y+z) ——>可以发现,当n=1时,x=z,也就是说,在相遇的位置设一个指针index,在头指针设一个慢指针,两者同时走,相遇时就为环入口;n>1时也相同
解答图

//进阶版
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};

三数之和(较难)

例题15:给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例

vector<vector<int>> threeSum(vector<int>& nums) {
//先排序,在一层for循环下,用双指针:如果三者之和大于0,则将右指针前移,否则将左指针后移
	sort(nums.begin(), nums.end());
	vector<vector<int>> res;
	int i, left, right;
	for (i = 0; i < nums.size(); i++)
	{
        if(i>0 && nums[i]==nums[i-1])//去重的关键步骤:跳过需要判断的和数
        {
            continue;
        }
		left = i + 1;
		right = nums.size() - 1;
		while (left < right)
		{
			if (nums[i] + nums[left] + nums[right] > 0)
			{
				right--;
			}
			else if (nums[i] + nums[left] + nums[right] < 0)
			{
				left++;
			}
			else
			{
				res.push_back(vector<int>{nums[i], nums[left], nums[right]});
				while (left < right && nums[right] == nums[right - 1])//去重的关键步骤:在一个固定的数下,跳过可能重复的和组合元素
				{
					right--;
				}
				while (left < right && nums[left] == nums[left + 1])
				{
					left++;
				}
				right--;
				left++;
			}
		}
	}
	return res;
    }
};

四数之和

例题18:给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。

示例

//两两互为相反数
	sort(nums.begin(), nums.end());
	vector<vector<int>> res;
	int n1, n2, left, right;
	for (n1 = 0; n1 < nums.size(); n1++)
	{
        //对n1剪枝
        if(nums[n1]>=0 && nums[n1]>target)
        {
            break;
        }
        //对n1去重
		if (n1 > 0 && nums[n1] == nums[n1 - 1])
		{
			continue;
		}
		for (n2 = n1 + 1; n2 < nums.size(); n2++)
		{
            //对n2剪枝:注意正数下大于目标才剪枝,负数下继续遍历
            if(nums[n1]+nums[n2]>=0 && nums[n2]+nums[n1]>target)
            {
                break;
            }
            //对n2去重
			if (n2 > n1+1  && nums[n2] == nums[n2 - 1])//去重不正确的原因在这里,n2的范围不对,会导致n1=n2时也被去掉
			{
				continue;
			}
			double sum = nums[n1] + nums[n2];
			double sum2 = target - sum;
			left = n2 + 1;
			right = nums.size() - 1;
			while (left < right)
			{
				if (nums[left] + nums[right] > sum2)
				{
					right--;
				}
				else if (nums[left] + nums[right] < sum2)
				{
					left++;
				}
				else
				{
					vector<int> a = { nums[n1], nums[n2], nums[left], nums[right] };
					sort(a.begin(),a.end());
					res.push_back(a);
					while (left < right && nums[right] == nums[right - 1])
					{
						right--;
					}
					while (left < right && nums[left] == nums[left + 1])
					{
						left++;
					}
					right--;
					left++;
				}
			}
		}
	}
	return res;
    }
};

思路与三数之和一样,难点在于怎么对前两位数去重?

双指针总结

数组篇

移除元素或翻转数组,用双指针在一个for循环下完成两个for循环的工作

字符串篇

erase()复杂度为O(n)

链表篇

N数之和篇

双指针主要用在后两数之和是否等于前面数之和的相反数,注意每次前后移动要去重
三数之和使用双指针,将复杂度从O(n^3) 降为O(n*n),同理四数之和也是如此

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值