编程题-去除重复字母(中等)

题目:

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

解法一(哈希表):

根据字典序最小的原理,应尽可能保证最小的字典序的字符出现在字符串前面(即字符串第一位尽可能小,字符串第二位在剩余字符串中也应尽可能小以满足题目要求),因此定义vector<char>保存出现的所有字符串并将其进行排序,定义unordered_map<char, int>保持出现所有字符的数量,用于确定满足题目要求的最小字典序的字符出现的位置。

在vector<char>排序字符中,先确定最小的字符是否满足题目要求(即在s中第一次出现时的位置,并保证vector<char>剩余包含的字符在后续中s中均可以查找到),通过哈希表计数,当s后续中不存在vector<char>剩余包含的字符时,该最小字典序不能作为最小字符,查找vector<char>第二小的字符,以此类推.....当最小字典序满足要求时,记录在s的索引位置(后续遍历查找均在该索引后进行),并在vector<char>删除满足要求的字符,进入下一次循环直至vector<char>中无剩余字符停止迭代。如下为笔者代码:

class Solution {
public:
    int getindex(string& s, unordered_map<char, int> hashTable1, char c, int index, vector<char> kyk){
        int length = s.size();
        int result=-1;
        int length_kyk = kyk.size();
        for(int i=0;i<index;i++){
            hashTable1[s[i]]--;
        }
        for(int i=index;i<length;i++){
            if(s[i]!=c){
                hashTable1[s[i]]--;
                if(hashTable1[s[i]]==0){     
                    return -1;
                }
            }
            else{
                return i;
            }
        }
        return result;
    }
    
    string removeDuplicateLetters(string s) {
        string result1="";
        vector<char> kyk;
        unordered_map<char, int> hashTable1;
        for(char c:s){
            auto it = hashTable1.find(c);
            hashTable1[c]++;
            if(it==hashTable1.end()){
                kyk.push_back(c);
            }
        }
        int length_kyk = kyk.size();
        int length = s.size();
        sort(kyk.begin(),kyk.end());
        int index=0;
        for(int i=0; i<length_kyk; i++){
            int a = getindex(s, hashTable1, kyk[i], index, kyk);
            if(a==-1){
                continue;
            }
            else{
                result1=result1+kyk[i];
                hashTable1.erase(kyk[i]);
                kyk.erase(kyk.begin()+i);
                length_kyk--;
                index = a;
                i=-1;
            }
        }
        return result1;
    }
};

解法二(贪心 + 单调栈):

首先考虑一个简单的问题:给定一个字符串s,如何去掉其中一个字符ch,使得得到的字符串字典序最小呢?答案是:找出最小的满足s[i]>s[i+1]的下表i,并去除字符s[i]。为了叙述方便,下面称这样的字符为【关键字符】。

我们从前向后扫描原字符串。每扫描到一个位置,我们就尽可能地处理所有的【关键字符】。假定在扫描位置s[i-1]之前的所有【关键字符】都已经被去除完毕,在扫描字符s[i]时,新出现的【关键字符】只可能出现在s[i]或者其后面的位置。

于是,我们使用单调栈来维护去除【关键字符】后得到的字符串,单调栈满足栈底到栈顶的字符递增。如果栈顶字符大于当前字符s[i],说明栈顶字符为【关键字符】,故应当被去除。去除后,新的栈顶字符就与s[i]相邻了,我们继续比较新的栈顶字符与s[i]的大小。重复上述操作,直到栈为空或者栈顶字符不大于s[i]。

我们还遗漏了一个要求:原字符串s中的每个字符都需要出现在新字符串中,且只能出现一次,为了让新字符串满足该要求,之前讨论的算法需要进行一下两点的更改:

1、在考虑字符s[i]时,如果它已经存在于栈中,则不能加入字符s[i]。为此,需要记录每个字符是否出现在栈中。

2、在弹出栈顶字符时,如果字符串在后面的位置上再也没有这一字符,则不能弹出栈顶字符。为此,需要记录每个字符的剩余数量,当这个值为0时,就不能弹出栈顶字符。

如下为利用贪心算法+单调栈数据结构实现的代码:

class Solution {
public:
    string removeDuplicateLetters(string s) {
        //num记录26个英文字符出现的次数
        //vis为记录每个字符是否出现在栈中(初始值为均0,1为在栈中,0为不在栈中)
        vector<int> vis(26), num(26);
        for (char ch : s) {
            num[ch - 'a']++;
        }
        //定义单调栈stk(string类型)
        string stk;
        for (char ch : s) {
            //判断vis站中是否存在相同的字符,如果栈中已出现,则不能加入字符ch
            if (!vis[ch - 'a']) {
                while (!stk.empty() && stk.back() > ch) {
                    if (num[stk.back() - 'a'] > 0) {
                        vis[stk.back() - 'a'] = 0;
                        stk.pop_back();
                    } else {
                        break;
                    }
                }
                vis[ch - 'a'] = 1;
                stk.push_back(ch);
            }
            num[ch - 'a'] -= 1;
        }
        return stk;
    }
};

时间复杂度:O(N),其中 N 为字符串长度。代码中虽然有双重循环,但是每个字符至多只会入栈、出栈各一次。空间复杂度:O(∣Σ∣),其中 Σ 为字符集合,本题中字符均为小写字母,所以 ∣Σ∣=26。由于栈中的字符不能重复,因此栈中最多只能有 ∣Σ∣ 个字符,另外需要维护两个数组,分别记录每个字符是否出现在栈中以及每个字符的剩余数量

笔者小记:

1、提示中s均为26个小写英文字符,则可以定义vector<int>数组用于计数和判断在某一(另一个数据结构)中存储出现情况。

2、相比利用哈希表,利用单调栈(string类型的)和vector<int>(技术和判断是否出现情况)可以更好的实现低时间复杂度的一次遍历。

3、vector<int> vis(26) 表示创建数组名为vis,数组大小为26的数组。

<think> 我们参考用户提供的引用内容,尤其是引用[1]和引用[2]中提到的重点和难点。用户希望得到一道中等难度的C语言指针编程题。根据引用[1],指针是重点和难点,因此我们设计一道涉及指针操作的题目。同时,引用[2]中提到了文件操作和字符串函数,但用户明确要求指针,所以我们专注于指针。 题目要求中等难度,我们可以设计一个题目,要求使用指针操作字符串或数组,并且需要一定的逻辑和指针操作技巧。 题目:编写一个C语言函数,实现字符串中单词的反转。例如,将字符串"Hello World"反转成"World Hello"。要求: 1. 不要使用额外的数组空间,必须使用指针原地操作。 2. 函数原型:void reverseWords(char *s); 3. 注意处理多个空格的情况,反转后单词间只保留一个空格,并且去除开头和结尾的多余空格。 这个题目涉及指针操作、字符串处理、就地修改,属于中等难度。 我们将提供题目描述以及一个参考的解答。 注意:由于用户可能需要上机练习,我们将提供完整的代码框架,包括main函数用于测试。 但是,用户要求的是编程题,所以我们先给出题目,然后给出参考答案,并附上简要解释。 题目描述: 给定一个字符串,逐个翻转字符串中的每个单词。要求原地修改字符串,不能使用额外的空间。单词由非空格字符组成,单词之间至少有一个空格分隔。反转后,单词间只保留一个空格,并且去除开头和结尾的空格。 示例: 输入:字符串" hello world " 输出:反转后字符串为"world hello" 函数原型:void reverseWords(char *s); 参考步骤: 1. 先反转整个字符串。 2. 然后逐个反转每个单词。 3. 同时处理多余空格,将单词之间的多个空格缩减为一个,并去除首尾空格。 由于是原地操作,我们需要使用指针来遍历和修改字符串。 参考代码: */ #include <stdio.h> #include <ctype.h> // 反转字符串中从start到end(包含)的部分 void reverse(char *start, char *end) { while (start < end) { char temp = *start; *start = *end; *end = temp; start++; end--; } } void reverseWords(char *s) { if (s == NULL || *s == '\0') return; // 第一步:反转整个字符串 char *start = s; char *end = s; while (*end) end++; end--; // 指向最后一个非空字符 reverse(start, end); // 第二步:逐个反转每个单词,并移除多余空格 char *dst = s; // 用于写的位置 char *ptr = s; // 用于读的位置 while (*ptr) { // 跳过空格 while (*ptr && isspace(*ptr)) ptr++; if (!*ptr) break; // 记录单词的开始和结束 char *word_start = ptr; while (*ptr && !isspace(*ptr)) ptr++; char *word_end = ptr - 1; // 反转这个单词 reverse(word_start, word_end); // 将这个单词移动到正确的位置(同时处理空格) if (dst != s) *dst++ = ' '; // 在单词前加空格(不是第一个单词) while (word_start <= word_end) { *dst++ = *word_start++; } } *dst = '\0'; // 字符串结束 } int main() { char str[] = " hello world "; printf("原始字符串: \"%s\"\n", str); reverseWords(str); printf("反转后: \"%s\"\n", str); return 0; } /* 代码说明: 1. reverse函数用于反转指定区间的字符串。 2. reverseWords函数首先反转整个字符串,然后遍历字符串,跳过空格,找到每个单词,反转单词,并同时去除多余空格。 3. 使用两个指针(dst和ptr)进行原地修改:dst指向当前写入的位置,ptr用于读取原字符串的位置。 4. 处理完所有单词后,在dst位置写入结束符'\0'。 注意事项: 这个程序假设输入字符串是可修改的(例如,传入的是字符数组,而不是字符串常量)。在main函数中,我们使用字符数组来测试。 这道题考察了指针操作、字符串处理、数组遍历和原地修改,属于中等难度,适合练习指针的使用。 相关问题: 1. 如果不允许使用isspace函数,该如何修改代码? 2. 如果要求不使用辅助函数(如reverse),如何实现? 3. 如何优化这个程序,减少指针操作的次数? </think> ### 中等难度的C语言指针编程题 **题目:字符串单词逆序** 编写一个函数 `void reverse_words(char *str)`,实现字符串中单词的逆序操作(单词顺序反转,但单词内部字母顺序不变)。要求: 1. 使用指针操作,不能使用数组下标语法(如 `str[i]`) 2. 原地修改字符串,不创建新数组 3. 处理多余空格:反转后单词间只保留一个空格,并去除首尾空格 **示例:** 输入:`" Hello World! This is C "` 输出:`"C is This World! Hello"` --- #### 参考实现代码 ```c #include <stdio.h> #include <ctype.h> // 反转字符串区间 [start, end] void reverse_range(char *start, char *end) { while (start < end) { char temp = *start; *start = *end; *end = temp; start++; end--; } } void reverse_words(char *str) { char *word_start = str; // 单词起始指针 char *dst = str; // 写入位置指针 // 步骤1:整体反转字符串 char *p = str, *q = str; while (*q) q++; reverse_range(p, q - 1); // 步骤2:逐个单词反转并处理空格 while (*p) { // 跳过空格 while (*p && isspace(*p)) p++; if (!*p) break; // 标记单词边界 word_start = p; while (*p && !isspace(*p)) p++; char *word_end = p - 1; // 反转单词 reverse_range(word_start, word_end); // 移动单词到目标位置 while (word_start <= word_end) { *dst++ = *word_start++; } if (*p) *dst++ = ' '; // 添加单空格分隔 } *dst = '\0'; // 终止字符串 } int main() { char str[] = " Hello World! This is C "; printf("原始: \"%s\"\n", str); reverse_words(str); printf("结果: \"%s\"\n", str); return 0; } ``` --- #### 考察重点解析 1. **指针操作核心** - 使用三重指针 (`str`, `p`, `dst`) 实现原地修改 - 区间反转通过双指针向中间逼近实现 - 空格处理依赖 `isspace()` 函数识别空白符 2. **关键步骤** ```mermaid graph LR A[整体反转字符串] --> B[遍历处理单词] B --> C{当前字符是空格?} C -->|是| D[跳过空格] C -->|否| E[标记单词边界] E --> F[反转单词] F --> G[移动到新位置] ``` 3. **难度要点** - 指针边界控制(如 `word_end = p - 1`) - 单词移动时的覆盖风险处理 - 空格压缩的指针逻辑(`dst` 滞后于 `p`) --- **相关问题** 1. 如何修改代码使其能处理标点符号(如逗号、句号)? 2. 如果要求保留原始空格数量,代码需要如何调整? 3. 指针操作相比数组下标有何性能优势?[^1] 4. 在文件操作中如何应用类似指针技巧处理文本?[^2] [^1]: 指针操作可直接访问内存地址,减少寻址计算开销,尤其适合字符串处理等场景。 [^2]: 文件读写函数如 `fgets`/`fputs` 结合指针移动,可实现高效文本处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值