蓝桥杯书的笔记(一:一些基础数据结构和算法的应用,C++)


https://www.lanqiao.cn/courses/3993/learning/?id=248897

将一段字符串转换成整数并存放在一个变量中(只能转数字)

//将一段字符串转换成整数并存放在一个变量中
int chnum(char str[])
{

    int i,n,num=0;
    for(i=0;str[i]!='\0';i++)
    {
        if(str[i]>='0' && str[i]<='9')
            num=num*10+str[i]-'0';

    }
    return num;
}

链表

小王子问题(含单双链表)

题意:
小王子有一天迷上了排队的游戏,桌子上有标号为 1-10 按顺序摆放的 10 个玩具,现在小王子想将它们按自己的喜好进行摆放。小王子每次从中挑选一个好看的玩具放到所有玩具的最前面。已知他总共挑选了 M 次,每次选取标号为 X 的玩具放到最前面,求摆放完成后的玩具标号。

给出一组输入,M=8 共计排了 8 次,这 8 次的序列为 9,3,2,5,6,8,9,8。 求最终玩具的编号序列。

#include <iostream>
using namespace std;
struct Node
{
    int data;
    Node *next;
};
Node *head = new Node; //先生成头结点
void init()
{
    head->next = nullptr; //形成空链,由上文已知单链表最后一个结点的指针为空。

    for (int i = 10; i >= 1; i--)
    {
        Node *temp = new Node;
        temp->data = i;

        temp->next = head->next;
        head->next = temp;
    }
}
void del(int x)
{
    Node *Befor = head;                                   //用于存放当前节点的前驱,因为单链表单向遍历,我们不能从下一个找到上一个
    for (Node *T = head->next; T != nullptr; T = T->next) //链表的遍历常用写法
    {
        if (T->data == x) //找到要的那个数了
        {
            Node *temp = T; //先临时保存结点

            Befor->next = T->next; //将节点从链表上摘除

            delete temp; //从内存中删除结点。

            return; //删除结束后,结束函数。
        }
        Befor = T; //前驱改变
    }
}
void insert(int x)
{
    Node *temp = new Node;
    temp->data = x;

    temp->next = head->next;
    head->next = temp;
}
void show(int i)
{
    cout << "这是第" << i << "次操作";
    for (Node *T = head->next; T != nullptr; T = T->next) //链表的遍历常用写法
    {
        cout << T->data << " ";
    }
    cout << endl;
}
int main()
{

    init();
    show(0);
    int N;
    cin >> N;
    for (int i = 1; i <= N; i++)
    {
        int x;
        cin >> x;
        del(x);
        insert(x);
        show(i);
    }
}

在这里插入图片描述

约瑟夫环问题

//头文件与命名空间
#include <iostream>
using namespace std;

struct Node
{
    int data;
    Node *pNext;
};

int main()
{
    int n, k, m, i; //n个人从k位置开始报数,数到m出列
     Node *p, *q, *head;
    cin >> n >> k >> m;
    if (m == 1)
    {
        for (int i = 0; i < n; i++) //共计N个人
        {
            int left;
            left = k - 1;
            if (left == 0)
                left = n;
            cout << left;
        }
    }

    else
    {

        Node * first = (Node *)new Node;
        p = first;
        first->data = 1;
        for (i = 2; i <= n; i++)
        {
            q = new Node;
            q->data = i;
            p->pNext = q;
            p = p->pNext;
        }
        p->pNext = first;
        p = first;
        for (i = 1; i <= k - 1; i++) //
            p = p->pNext;
        while (p != p->pNext) //只剩下一个结点的时候停止
        {
            for (i = 1; i < m - 1; i++)
            {
                p = p->pNext;
            }

            q = p->pNext; //q为要出队的元素
            cout << q->data << endl;
            p->pNext = q->pNext;
            delete q;
            p = p->pNext;
        }
        cout << p->data << endl; //输出最后一个元素
    }

    return 0;
}

小王子问题双链表

#include <iostream>
using namespace std;
struct Node
{
    int data;
    Node *next;
    Node *before;
};
Node* head = new Node; //先生成头结点
void insert(int x)
{
    Node* temp=new Node;
    temp->data=x;

    temp->next=head->next;
    head->next=temp;
    temp->before=head;
    if(temp->next)     temp->next->before=temp;
}


void init() //为了美观,我们写个初始化函数
{
    head->next=nullptr; //无论用什么方式,都不能省略该语句,不然无法正常使用。
    head->before=nullptr;
    for(int i=10; i>=1; i--) insert(i); //从10开始插入
}
void del(int x)
{

    for(Node*T=head->next; T!=nullptr; T=T->next) //链表的遍历常用写法
    {
        if(T->data==x)   //找到要的那个数了
        {

            T->before->next=T->next;//双向链表,就是如此简单方便。
            T->next->before=T->before;
            return; //删除结束后,结束函数。
        }

    }
}
void show(int i)
{
    cout << "这是第" << i << "次操作";
    for (Node *T = head->next; T != nullptr; T = T->next) //链表的遍历常用写法
    {
        cout << T->data << " ";
    }
    cout << endl;
}

int main()
{

    init();
    show(0);
    int N;
    cin >> N;
    for (int i = 1; i <= N; i++)
    {
        int x;
        cin >> x;
        del(x);
        insert(x);
        show(i);
    }
}

队列

思考一下:

为什么链式存储的方式的队列首尾指针与链表头尾刚好相反,是什么原因呢?

其实我们知道链表的表头是用来插入数据的,表尾处的数据才是最先插入的,先入先出原则,所以表尾出的数据最先出列,也就是队列的头啦!听到这里,可能有人迷糊了,什么头什么尾的?链表是数据存储的组织方式,他只是决定了数据在内存中怎么存储,而队列是说我们是按照什么顺序存储。可以理解为一群人排队,队列告诉他们先来的先吃饭,后来的得排队,而链表或顺序表是说,你可以站着排队蹲着排队等等。

银行排队问题(普通队列)

题意:
银行排队问题,CLZ 银行只有两个接待窗口,VIP 窗口和普通窗口,VIP 用户进入 VIP 用户窗口,剩下的进入普通窗口排队。

现在有以下操作:

第一行 M 次操作(M<1000)

第二行 到 第M+1行 输入操作

格式:   IN name V
        OUT V
        IN name2 N
        OUT N
        即 第一个字符串为操作 是IN进入排队和OUT 出队
            IN 排队 跟着两个字符串为姓名和权限V或N
            OUT 为出队即完成操作,V和N代表那个窗口完成了操作

输出:M次操作后V队列和N队列中姓名,先输出V队列后输出N队列。

样例:

输入:

5
IN xiaoming N
IN Adel V
IN laozhao N
OUT N
IN CLZ V

输出:

Adel
CLZ
laozhao
//注意题意,输出队伍中现有的

代码:

#include <iostream>
using namespace std;
string Vqueue[1005]; //V队列
int Vhead=0;         //首指针
int Vtail=0;         //尾指针

string Nqueue[1005]; //N队列
int Nhead=0;         //首指针
int Ntail=0;         //尾指针

void in(string name,string type)
{
    if(type=="V"){
        Vqueue[Vtail]=name;
        Vtail++;
    }
    else
    {
        Nqueue[Ntail]=name;
        Ntail++;
    }
}

bool out(string type)
{
    if(type=="V"){
        if(Vhead==Vtail) {
            //队伍没有人不能在出队了。
            return false ;
        }
        else{
            Vhead++;//head前的数据都是无效数据,无需删除,逻辑明确即可。
            return true;
        }

    }
    else
    {
        if(Nhead==Ntail) {
            //队伍没有人不能在出队了。
             return false;
        }
        else{
            Nhead++;//head前的数据都是无效数据,无需删除,逻辑明确即可。
            return true;
        }
    }
}

string  getHead(string type)
{

    if(type=="V"){
        return Vqueue[Vhead];
    }
    else
    {
       return Nqueue[Nhead];
    }
}

int main()
{
    int M;
    cin>>M;

    while(M--) //
    {
        string op,name,type;
        cin>>op;
        if(op=="IN")
        {
            cin>>name>>type;
            in(name,type);
        }
        else
        {
            cin>>type;
            out(type);
        }
    }
    string s=getHead("V");
    while(out("V"))
    {
        cout<<s<<endl;
        s=getHead("V");
    }
    s=getHead("N");
    while(out("N"))
    {
        cout<<s<<endl;
        s=getHead("N");
    }

}

使用循环队列解决 CLZ 银行的问题

当所需入队的数据很大时,我们空间一定时,那么普通队列对空间的低效率利用就显得很蹩脚,所以提出了循环队列的方式。(链式队列就没有这个缺点,所以后续课程中我们讲的容器或者类都采用链式队列的方式实现。)

其实对于不存在物理上实现的循环结构,我们可以用软件方法实现(采用求模方式):

  • tail=(tail+1)% MAXSIZE
  • head=(head+1) % MAZSIZE

需要注意:
1.如何判断循环队列队为空?

队空:head == tail 跟之前一样。

2.如何判断循环队列队为满?

队满:(tail+1) mod QueueSize==head

3.如何获得队列中的元素数量

length=(tail-head+QueueSize)%Queuesize

由于顺序存储队列必须预先确定一个固定的长度,所以存在存储元素个数的限制和空间浪费的问题。

代码:

#include<iostream>
using namespace std;

const int QueueSize=10005;
string Vqueue[QueueSize];
int Vhead;
int Vtail;
string Nqueue[QueueSize];
int Nhead;
int Ntail;

bool in(string name,string type)
{
    if(type=="V"){
        if ((Vtail+1) % QueueSize ==Vhead) return false;
        //队列以达到容量上限满了,所以不能再插入了返回错误;
        else{
            Vtail=(Vtail+1) % QueueSize;
            Vqueue[Vtail]=name;
            return true;
       }
    }
    else
    {
        if ((Ntail+1) % QueueSize ==Nhead) return false;
        //队列以达到容量上限满了,所以不能再插入了返回错误;
        else{
            Ntail=(Ntail+1) % QueueSize;
            Nqueue[Ntail]=name;
            return true;
       }
    }
}

bool out(string type)
{
    if(type=="V"){
        if (Vtail==Vhead) return false;
        //空队列不能出队列了

        else {
            Vhead=(Vhead+1) % QueueSize;
             //不是空队列,但是因为是循环的,如果到了数组末尾也要调整到前面去。
            return true;
        }
    }
    else
    {
        if (Ntail==Nhead) return false;
        //空队列不能出队列了

        else {
            Nhead=(Nhead+1) % QueueSize;
             //不是空队列,但是因为是循环的,如果到了数组末尾也要调整到前面去。
            return true;
        }
    }
}

string  getHead(string type)
{
    if(type=="V"){
        if (Vtail==Vhead) return "";//空队列返回空
        else {
            return Vqueue[Vhead+1];
        }
    }
    else
    {
        if (Ntail==Nhead) return "";//空队列返回空
        else {
            return Nqueue[Nhead+1];
        }
    }
}
int main()
{
    int M;
    cin>>M;

    while(M--) //
    {
        string op,name,type;
        cin>>op;
        if(op=="IN")
        {
            cin>>name>>type;
            in(name,type);
        }
        else
        {
            cin>>type;
            //cout<<out(type)<<endl;
        }
    }

    while(getHead("V")!="")
    {
        cout<<getHead("V")<<endl;;
        out("V");
    }

    while(getHead("N")!="")
    {
        cout<<getHead("N")<<endl;
        out("N");
    }

}

另外,大家从我们上面写过的代码可以看出,其中设置了很多输出,用于调试代码,满是代码调试的痕迹。其实每个人写代码都不是一蹴而就的,在后续的学习中,希望大家在觉得复杂困难的部分,要想办法解决,不要因为困难就放弃。

小邋遢的衣橱

题意:
小邋遢 MS.Jinlin 是个爱打扮的公主,他有很多晚礼服如"LALA" “NIHAOMA”、“WOBUHAO”、"NIHAOBUHAO"等众多衣服,可是由于衣服太多他要把它们装进箱子,但是作为公主,肯定是会突发奇想觉得哪件衣服好看,就把他拿了出来,当然那件衣服上面的衣服也被拿出来了,而且会弄乱了,小邋遢在经过几次的叠衣服和取衣服后,他想知道箱子里最上面的衣服是哪一件,如果箱子为空的话,就告诉她 Empty ,如果有多件一样的衣服,肯定是取走最上面的那一件啦。

输入:

1 行,输入N,代表共计进行了几次操作
第 2 行至第 N+1 行,进行in out 操作
in 为 放入衣服
out 为 取出衣服

格式:  

    in name1  
    out name2

样例1:

输入:

6
in AMDYES
in INTELNO
in USBAD
in CNYES
out INTELNO
in MDICN

输出:

MDICN

样例2:

输入:

5
in AMDYES
in INTELNO
in USBAD
in CNYES
out AMDYES

输出:
Empty

代码:


#include<iostream>
#include<string>
using namespace std;


const int maxsize=100005;

string Mystack[maxsize]; //栈
int Top=0;         //栈顶指针

bool in(string name)
{
    if(Top>=maxsize) return 0;

    else 
    {
        Mystack[++Top]=name;
        return 1;
    }

}
bool isEmpty()
{


    if(Top!=0) return 0;

    else return 1;
}

bool out()
{
    if(isEmpty()) return 0;

    else{
        Top--;
        return 1;
    }
}


string getTop()
{
    if(isEmpty()) return "";

    else return Mystack[Top];
}


int main ()
{
    int N;
    cin>>N;

    for(int i=0;i<N;i++)
    {
        string op,name;

        cin>>op>>name;

        if(op=="in") in(name);

        else 
        {

            while(getTop()!=name)
            {
                out();
            }

            out();
        }
    }

    if(isEmpty) cout<<"Empty"<<endl;
    else cout<<getTop()<<endl;
}

C++ 的内置栈模板(解决上题)

C++ 中栈的定义及相应的函数内容:

LIFO stack 堆栈,它是一种容器适配器,专门设计用于在 LIFO 上下文(后进先出)中操作,其中元素仅从容器的一端插入和提取。

stack 被实现为容器适配器 它们是使用特定容器类的封装对象作为其类底层容器的 ,提供一组特定的成员函数来访问其元素。元素推入 / 弹出 从 的 “后面” 特定容器 ,这被称为 的顶部堆栈。

底层容器可以是任何标准容器类模板或一些其他专门设计的容器类。

在 C++ 的 stack 模板定义了如下操作:

  • top():

返回一个栈顶元素的引用,类型为 T&。如果栈为空,返回值未定义。

  • push(const T& obj):

可以将对象副本压入栈顶。这是通过调用底层容器的 push_back() 函数完成的。

  • push(T&& obj):

以移动对象的方式将对象压入栈顶。这是通过调用底层容器的有右值引用参数的 push_back() 函数完成的。

  • pop():

弹出栈顶元素。

  • size():

返回栈中元素的个数。

第一步:

引入模板类,并定义声明一个栈类

#include
#include
using namespace std;
stack myStack;
copy
第二步:

我们要声明并定义入栈函数:

按照栈的定义使用栈顶指针模拟即可
需要传入一个参数来表示放什么数据
这里我们 C++ 的 stack 模版中已经为我们声明并定义好了,所以我们不需要写,这一步可以省略。

第三步:

我们声明并定义判空函数:

这里我们 C++ 的 stack 模版中已经为我们声明并定义好了,所以我们不需要写,这一步可以省略。

第四步:

需要要声明并定义出栈函数:

按照栈的定义使用栈顶指针模拟即可
返回一个数据表示出栈元素。
这里我们 C++ 的 stack 模版中已经为我们声明并定义好了,所以我们不需要写,这一步可以省略。

第五步:

我们声明并定义取栈顶函数:

只需要将栈顶元素取出即可
先判断是否为空
这里我们 C++ 的 stack 模版中已经为我们声明并定义好了,所以我们不需要写,这一步可以省略。

第六步:

主函数代码:

完整代码:

#include <iostream>
#include <stack>
using namespace std;

stack<string> myStack;

int main ()
{
    int N;
    cin>>N;
    for(int i=0;i<N;i++)
    {
        string op,name;
        cin>>op>>name;
        if(op=="in") myStack.push(name);
        else {
            while(myStack.top()!=name){
                myStack.pop();
            }
            myStack.pop();
        }
    }
    if(myStack.empty()) cout<<"Empty"<<endl;
    else cout<<myStack.top()<<endl;
}

散列表(Hash),查找的一部分

知识点

  • Hash 的概念
  • 构造方法
  • 冲突处理

思考:

  • 散列技术仅仅是一种查找技术吗?

    应该说,散列既是一种查找技术,也是一种存储技术。

  • 散列是一种完整的存储结构吗?

    散列只是通过记录的关键码定位该记录,没有完整地表达记录之间的逻辑关系,即通过关键码能推出 Key 值,但是通过关键码对应的值(即位置处的值)不能推出关键码,所以散列存储的关键码和值之间并不对称,因此散列主要是面向查找的存储结构。

弗里的语言

题意:
小发明家弗里想创造一种新的语言,众所周知,发明一门语言是非常困难的,首先你就要克服一个困难就是,有大量的单词需要处理,现在弗里求助你帮他写一款程序,判断是否出现重复的两个单词。

要求:有重复的单词,就输出重复单词,没有重复单词,就输出 NO,多个重复单词输出最先出现的。

第 1 行,输入 N,代表共计创造了多少个单词
第 2 行至第 N+1 行,输入 N 个单词

格式如下:

fjsdfgdfsg
fdfsgsdfg
bcvxbxfyres

样例 1:

输入:

6
1fagas 
dsafa32j
lkiuopybncv
hfgdjytr
cncxfg
sdhrest

输出:

NO

样例 2:

输入:

5
sdfggfds
fgsdhsdf
dsfhsdhr
sdfhdfh
sdfggfds


输出:

sdfggfds

除留余数法


散列表具有高性能,查找效率高等优点 散列表并不是适用于所有的需求场景,那么哪些情况下不适合使用呢?

散列技术一般不适合在允许多个记录有同样关键码的情况下使用。

因为这种情况下,通常会有冲突存在,将会降低查找效率,体现不出散列表查找效率高的优点。

并且如果一定要在这个情况下使用的话,还需要想办法消除冲突,这将花费大量时间,那么就失去了 O(1) 时间复杂度的优势,所以在存在大量的冲突情况下,我们就要弃用散列表。

散列方法也不适用于范围查找,比如以下两个情况。

查找最大值或者最小值

因为散列表的值是类似函数的,映射函数一个变量只能对应一个值,不知道其他值,也不能查找最大值、最小值,RMQ(区间最值问题)可以采用 ST 算法、树状数组和线段树解决。

也不可能找到在某一范围内的记录

比如查找小于 N 的数有多少个,是不能实现的,原因也是映射函数一个变量只能对应一个值,不知道其他值。


散列技术的关键问题

在使用散列表的时候,我们有两个关键的技术问题需要解决:

散列函数的设计,如何设计一个简单、均匀、存储利用率高的散列函数?
冲突的处理,如何采取合适的处理冲突方法来解决冲突。
如何设计实现散列函数
在构建散列函数时,我们需要秉持两个原则:

	简单

散列函数不应该有很大的计算量,否则会降低查找效率。

		均匀:

函数值要尽量均匀散布在地址空间,这样才能保证存储空间的有效利用并减少冲突。

散列函数实现三种方法

  1. 直接定址法。

散列函数是关键码(Key)的映射的线性函数,形如:

H(key) = a * key + bH(key)=a∗key+b

缺点:

我们是看到了这个集合,然后想到他们都是 11 的倍数才想到这 Hash 函数。我们在平常的使用中一般不会提前知道 Key 值集合,所以使用较少。
适用范围:

事先知道关键码,关键码集合不大且较为连续而不离散。

  1. 除留余数法。
    在这里插入图片描述
    缺点:

P 选取不当,会导致冲突率上升。
适用范围:

除留余数法是一种最简单、也是最常用的构造散列函数的方法,并且不要求事先知道关键码的分布。
这个方法非常常用,我们后面题目的展开就是使用的这个方法。在大部分的算法实现中也都是选取的这一种方式。

代码:

c++
    const int MOD=P;

    int Hx(int n)
    {
        return n%MOD;
    }

Java
        
    final Integer MOD=P;

    Integer Hx(int n)
    {
        return n%MOD;
    }

 python 

    MOD=P #由于Python不含常量,我们这里就不做修饰

    Hx(n):
        global MOD
        return n%MOD
  1. 数字分析法。
    比如我将我的集合全部转化为 16 进制数,根据关键码在各个位上的分布情况,选取分布比较均匀的若干位组成散列地址。或者将 N 位 10 进制数,观察各各位的数字分布,选取分布均匀的散列地址。
    既然叫做数字分析法,那么只有对于不同数据的不同分析,才能写出更是适配的 H(x)。

另外还有两种平时使用极少的方法,分别是平方取中法和折叠法,

冲突的处理方法
开散列方法:线性探测法
二次探测法
随机探测法
再 hash 法

闭散列方法:拉链法(链地址法)

我们这里介绍一种在算法竞赛中特别常用的字符串映射成数字的方式。
在这里插入图片描述
代码:

const long long h = 12582917;

int Hx(string s)
{

    int n = s.size();
    int sum1 = 0;

    for (int i = 0; i < n; i++)
    {
        sum1 = sum1 * 131 % h + (s[i] - 'a' + 1) % h;
    }

    return (sum1 + h) % h;
}
/*在比赛按此方法设计 Hash 函数
一般不需要设置冲突的公共溢出区,这里我们为了方便讲解,
才进行设置,在比赛中我们不用设置溢出区,
所以可以设置很大的 h,避免出现冲突。*/

定义查询函数:

通过散列表顶指针大小即可判断。

bool isAt(string s)
{
    int n=Hx(s);

    if(Value[n]=="") 
        return false;

    else if(Value[n]==s) 
        return true;

    else {
        for(int i=0;i<UpValueCount;i++)
            if(UpValue[n]==s) return true;
            
        return false;
    }

}

第四步,定义插入散列表函数:

按照散列表的映射方式设计即可;
需要传入一个参数来表示放什么数据。

bool in(string s)
{
    int n=Hx(s);
    if(Value[n]=="") {
        Value[n]=s;
        return true;
    }
    else if(Value[n]==s) return false;
    else {
        for(int i=0;i<UpValueCount;i++)
            if(UpValue[n]==s) return false;

        UpValue[UpValueCount++]=s;
        return true;
    }
}

主函数代码有三种,一种比一种简单,但是是根据具体题意来简化的。
F1:中规中矩定义法,设置 flag 变量用于跳过找到答案后的输入处理。

int main()
{

    int n;
    bool flag = 0;
    string ans = "NO";
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        string word;
        cin >> word;
        if (flag)
            continue;

        if (isAt(word))
        {
            flag = 1;
            ans = word;
        }
        else
        {
            in(word);
        }
    }
    cout << ans << endl;
}

F2:由于我们设置的插入函数也具有查询功能,插入成功即为没有重复值,插入失败即为有重复值,我们这里不存在单独查询的操作,所以我们可以将查询省略。


int main()
{

    int n;
    bool flag = 0;
    string ans = "NO";
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        string word;
        cin >> word;
        if (flag)
            continue;

        if (!in(word))
        {
            flag = 1;
            ans = word;
        }
    }
    cout << ans << endl;
}

F3:在法二的基础上,利用 OJ 的特性,OJ 是判定输出的答案是否与答案相同进行判定,当我们知道答案之后直接输出,结束程序那么就会使得程序运行时间大幅度减少。


int main()
{

    int n;
    string ans = "NO";
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        string word;
        cin >> word;

        if (!in(word))
        {
            cout << word << endl;
            return 0;
        }
    }
    cout << ans << endl;
}

完整代码;

#include <iostream>
#include <stack>
using namespace std;

const int h = 12582917;

string Value[h];

string UpValue[h];

int UpValueCount = 0;

int Hx(string s)
{

    int n = s.size();

    int sum1 = 0;

    for (int i = 0; i < n; i++)
    {

        sum1 = sum1 * 131 % h + (s[i] - 'a' + 1) % h;
    }

    return (sum1 + h) % h;
}

bool isAt(string s)
{
    int n = Hx(s);
    if (Value[n] == "")
        return false;
    else if (Value[n] == s)
        return true;
    else
    {
        for (int i = 0; i < UpValueCount; i++)
            if (UpValue[n] == s)
                return true;

        return false;
    }
}

bool in(string s)
{
    int n = Hx(s);
    if (Value[n] == "")
    {
        Value[n] = s;
        return true;
    }
    else if (Value[n] == s)
        return false;
    else
    {
        for (int i = 0; i < UpValueCount; i++)
            if (UpValue[n] == s)
                return false;

        UpValue[UpValueCount++] = s;
        return true;
    }
}

int main()
{

    int n;
    string ans = "NO";
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        string word;
        cin >> word;

        if (!in(word))
        {
            cout << word << endl;
            return 0;
        }
    }
    cout << ans << endl;
}

最后总结给大家一个小窍门,在解题过程中可以使用:

C++ 中有一个 UnorderedMap,可以方便我们的解题过程;
Python 和 Java 中都有提前定义好的 Hash 函数,也可以直接使用。

排序

简单排序算法包括选择排序、冒泡排序、桶排序和插入排序
简单排序算法包括选择排序、冒泡排序、桶排序和插入排序

选择排序:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素,按照顺序放在待排序的数列的最前,直到全部待排序的数据元素排完。


#include <iostream>
#include <stack>
using namespace std;

void select_Sort(int *a,int len)
{
    for (int i=0;i<len;i++)
    {
        int k=i;
        for(int j=i+1;j<len;j++)
        {
            if(a[j]<a[k]) k=j;
        }
        if(k!=i) swap(a[i],a[k]);
    }
}
int main ()
{

    int a[] = {5 ,4 ,6 ,8 ,7, 1, 2 ,3};
    select_Sort(a,8);
    for(auto i: a) cout<<i<<" ";

}

冒泡排序
所谓冒泡排序就是依次将两个相邻的数进行比较,大的在前面,小的在后面。

#include <iostream>
using namespace std;

void BubbleSort(int arr[], int n)
{
    for(int i = 0; i < n - 1; i++)
    {
        for(int j = 0; j < n - i - 1; j++)
        {
            if(arr[j] > arr[j+1])
               swap(arr[j],arr[j+1]);
        }
    }
}

int main ()
{

    int a[6] = {4, 5, 6, 1, 2, 3};
    BubbleSort(a,6);
    for(auto i: a) cout<<i<<" ";

}

桶排序
桶排序的思想是,若待排序的记录的关键字在一个明显有限范围内时,可设计有限个有序桶,每个桶只能装与之对应的值,顺序输出各桶的值,将得到有序的序列。简单来说,在我们可以确定需要排列的数组的范围时,可以生成该数值范围内有限个桶去对应数组中的数,然后我们将扫描的数值放入匹配的桶里的行为,可以看作是分类,在分类完成后,我们需要依次按照桶的顺序输出桶内存放的数值,这样就完成了桶排序。

int a[10];
int n;
cin>>n
for(int i=0;i<n;i++)
{
    int key;
    cin>>key;
    a[key]++;
}
for(int i=0;i<n;i++)
{
    for(int j=0;j<a[i];j++)
    {
        cout<<i<<" ";
    }
}

插入排序
插入排序是一种简单的排序方法,时间复杂度为 O(n*n),适用于数据已经排好序,插入一个新数据的情况。其算法的基本思想是,假设待排序的数据存放在数组 a[1…n] 中,增加一个节点 x 用于保存当前数据,进行比较,a[1]即作为有序区,a[2…n] 作为无序区。


#include <iostream>
#include <stack>
using namespace std;

void insert_Sort(int *a,int len)
{
    for (int i=0; i<len; i++)
    {
        int x = a[i];
        int j = i - 1;
        while( j>=0&&x < a[j])
        {
            a[j + 1] = a[j];
            j -= 1;
        }
        a[j + 1] = x;
    }
}

int main ()
{

    int a[9] = {0, 3, 2, 4, 1, 6, 5, 2, 7};
    insert_Sort(a,9);
    for(auto i: a) cout<<i<<" ";

}

高效排序算法:

快速排序
快速排序是一种采用分治法解决问题的一个典型应用,也是冒泡排序的一种改进。它的基本思想是,通过一轮排序将待排记录分割成独立的两部分,其中一部分均比另一部分小,则可分别对这两部分继续进行排序,已达到整个序列有序。排序的时间复杂度为 O(nlogn),相比于简单排序算法,运算效率大大提高。


#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;

int tem[10000];

void part(int l, int r, int *a);

void qSort(int *a, int len)
{
    part(0, len - 1, a);
}
 
void part(int l, int r, int *a)
{
    if(l>=r) return ;
    int r1 = r, l1 = l;
    while (l1 < r1)
    {
        while (a[r1] >= a[l1]&&l1 < r1)
            r1--;
        if (l1 < r1)
            swap(a[l1], a[r1]);
        else
            break;
        while (a[l1] <= a[r1]&&l1 < r1)
            l1++;
        if (l1 < r1)
            swap(a[l1], a[r1]);
        else
            break;
    }
    part(l,l1-1,a);
    part(l1+1,r,a);
}
int main()
{
    int a[1000];
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    qSort(a, n);
    for (int i = 0; i < n; i++)
    {
        cout << a[i] << " ";
    }
}

归并排序
归并排序是由递归实现的,主要是分而治之的思想,也就是通过将问题分解成多个容易求解的局部性小问题来解开原本的问题的技巧。

归并排序在合并两个已排序数组时,如果遇到了相同的元素,只要保证前半部分数组优先于后半部分数组, 相同元素的顺序就不会颠倒。所以归并排序属于稳定的排序算法。

每次分别排左半边和右半边,不断递归调用自己,直到只有一个元素递归结束,开始回溯,调用 merge 函数,合并两个有序序列,再合并的时候每次给末尾追上一个最大 int 这样就不怕最后一位的数字不会被排序。


  #include <iostream>
  #include <cmath>
  #include <iomanip>
  #include <algorithm>
  using namespace std;
  #define MAX 10000 
  int tem[MAX];
  void merge(int a[], int b[], int l1, int l2, int end)
  {
    int p1 = l1, p2 = l2, p3 = l1;
    while (p1 <l2 && p2 < end)
    {
        if (b[p1] > b[p2])
            a[p3++] = b[p2++];
        else
            a[p3++] = b[p1++];
    }
        while (p2 < end)
            a[p3++] = b[p2++];
        while (p1 < l2)
            a[p3++] = b[p1++];
    for (int i = l1; i < end; i++){
        b[i] = a[i];
      // cout<<b[i]<<" ";
    }
   //  cout<<endl;
  }
  void merge_sort(int l, int r, int a[])
  {
   
    if (l >= r - 1)
        return;
    //cout<<"guibing"<<l<<" "<<r<<endl;
    int mid = (r + l) / 2;
    merge_sort(l, mid, a);
    merge_sort(mid, r,a);
    merge(tem, a, l, mid , r);
  }
  int main()
  {
    int n, a[MAX];
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    merge_sort(0, n, a);
    for (int i = 0; i < n; i++)
    {
        cout<<a[i]<<" ";
    }
  }

希尔排序
希尔排序是非稳定排序算法,同时也突破了之前内排序算法复杂度为 O(n2)的限制。
先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。

其中增量序列的选择是非常关键的,但通常我们取步长为 n/2(数组长度的一般)然后一直取半直到 1。

#include <iostream>
using namespace std;

void ShellSort(int array[], int n) //希尔排序函数
{
    int i, j, step = n / 2;
    while (step > 0) //这里的step步长是根据10个元素这种情况定义的
    {
        for (i = 0; i < step; i++) //i是子数组的编号
        {
            for (j = i + step; j < n; j = j + step) //数组下标j,数组步长下标j+step
            {
                if (array[j] < array[j - step])
                {
                    int temp = array[j]; //把数组下标j的值放到temp中
                    int k = j - step;
                    while (k >= 0 && temp < array[k])
                    {
                        array[k + step] = array[k]; //把大的值往后插入
                        k = k - step;
                    }
                    array[k + step] = temp; //把小的值往前插入
                }
            }
        }
        step = step / 2;
    }
}

int main(void) //主程序
{

    int array[1000], n;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> array[i];
    }

    ShellSort(array, n);
    for (int i = 0; i < n; i++)
        cout << array[i] << " ";

    return 0;
}

在我们编译语言中,都是预先设置好排序算法的,我们只需要直接调用即可。
但是有些情况是不能调用排序算法的,比如特殊的结构体排序而且要求是稳定的这种情况,
所以需要我们在上面各种排序算法的原理的基础上进行改写。大部分情况下我们都是可以直接调用的。

我们将使用 sort 函数解决该问题,由于 sort 在 algorithm 头文件里面,所以使用前先要调入头文件。

用法 sort(首地址,尾地址后面一个位置)

尾地址后面一个位置,即首地址+长度

比如我想排序 a 的第五个元素到第八个元素,共四个元素

那么首地址为 a+5,尾地址后面的一个地址为 a+5+4

尾地址后面一个位置这么描述是为了好理解,其实很多函数都是这么定义的,先这样记住就行,后边就写成 sort(a,a+n)。

或者是 sort(a,a+n,cmp) ,其中 cmp 是比较函数可以根据所比较的数据类型写出比较函数。返回值为 bool 值即可。

内置模板

知识点

  • 迭代器讲解
  • 线性表的使用
  • 队列的使用
  • 集合(set)的使用
  • 映射(map)的使用

迭代器(Iterator)

对于数组我们可以采用指针进行访问,但是对于其他的存储空间连续的数据结构或者说是存储单元我们就需要找到另一种方式来替代指针的行为作用,从而达到对于非数组的数据结构的访问和遍历,于是我们定义了一种新的变量叫做迭代器。

定义:

迭代器是一种检查容器内元素并遍历元素的数据类型。

迭代器提供对一个容器中的对象的访问方法,并且定义了容器中对象的范围。

Vector 容器(类)

线性表中有 Vector 和 list,两者作用比较相似。

Vector 的主要作用就是可变长度的数组,就把他当成数组使用即可。

至于为甚我们我选择讲 Vector 而不是 List,因为 Vector 可以当作数组使用,用起来非常简单,也非常方便。

我们先讲解一下 c++ 的 Vector 使用:

#include <vector>   //头文件
vector<int> a;      //定义了一个int类型的vector容器a
vector<int> b[100]; //定义了一个int类型的vector容器b组
struct rec
{
    ···
};
vector<rec> c;            //定义了一个rec类型的vector容器c
vector<int>::iterator it; //vector的迭代器,与指针类似

具体操作:
a.size()           //返回实际长度(元素个数),O(1)复杂度
a.empty()      //容器为空返回1,否则返回0,O(1)复杂度
a.clear()      //把vector清空
a.begin()      //返回指向第一个元素的迭代器,*a.begin()与a[0]作用相同
a.end()        //越界访问,指向vector尾部,指向第n个元素再往后的边界
a.front()      //返回第一个元素的值,等价于*a.begin和a[0]
a.back()       //返回最后一个元素的值,等价于*--a.end()和a[size()-1]
a.push_back(x) //把元素x插入vector尾部
a.pop_back()   //删除vector中最后一个元素

遍历的方式有两种:

1.迭代器使用与指针类似,可如下遍历整个容器。

 for ( vector<int>::iterator it=a.begin() ; it!=a.end() ; it++ )
	 cout<<*iterator<<endl;

2.当成数组使用。

for( int i=0;i<a.size();i++) cout<<a[i]<<endl;

题目

快递员需要对快递进行分拣,现在小李是一名快递员,他想要你帮他设计一个程序用于快递的分拣,按城市分开。

现在有以下输入:

单号 省份

请你将单号按照城市分开,并输出。

城市按照输入顺序排序

单号按照输入顺序排序

样例如下:

输入

10 

10124214 北京
12421565  上海
sdafasdg213 天津
fasdfga124 北京
145252  上海
235wtdfsg 济南
3242356fgdfsg 成都
23423 武汉  
23423565f 沈阳
1245dfwfs 成都

输出

北京 2
10124214
fasdfga124
上海 2
12421565
145252
天津 1
sdafasdg213
济南 1
235wtdfsg
成都 2
3242356fgdfsg 
1245dfwfs 
武汉 1
23423
沈阳 1
23423565f 

完整代码:

#include<iostream>
#include<vector>
using namespace std;

vector<string> city;//我们创建一个 vector 用于保存地址

vector<string> dig[1000];//我们创建一个 vector 组用于存放单号

int Myfind(string s)//我们定义一个映射函数,因为你的城市可能会再次出现,你需要知道之前有没有。
{

    for(int i=0;i<city.size();i++)
    {
        if(city[i]==s) return i;
    }

    return -1;
}
int main()//我们开始读入操作并按照顺序进行存放
{

    int n;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        string d,c;
        cin>>d>>c;
        int flag=Myfind(c);
        if(flag==-1){
            city.push_back(c);
            dig[city.size()-1].push_back(d);

        }
        else  dig[flag].push_back(d);
    }
    for(int i=0;i<city.size();i++)
    {
        cout<<city[i]<<" "<<dig[i].size()<<endl;

        for(int j=0;j<dig[i].size();j++)
            cout<<dig[i][j]<<endl;
    }
}

队列 Queue

定义方式:在 C++ 里所有容器的定义方式基本一致。

queue<string> myqueue;
queue<int> myqueue_int;

成员函数:

  • front():返回 queue 中第一个元素的引用。
  • back():返回 queue 中最后一个元素的引用。
  • push(const T& obj):在 queue 的尾部添加一个元素的副本。
  • pop():删除 queue 中的第一个元素。
  • size():返回 queue 中元素的个数。
  • empty():如果 queue 中没有元素的话,返回 true。

CLZ 的银行。

第一行 M 次操作(M<1000)

第二行 到 第M+1行 输入操作

格式:   IN name V
        OUT V
        IN name2 N
        OUT N
        即 第一个字符串为操作 是IN进入排队和OUT 出队
            IN 排队 跟着两个字符串为姓名和权限V或N
            OUT 为出队即完成操作,V和N代表那个窗口完成了操作

输出:M次操作后V队列和N队列中姓名,先输出V队列后输出N队列。

样例:

输入:

5
IN xiaoming N
IN Adel V
IN laozhao N
OUT N
IN CLZ V

输出:

Adel
CLZ
laozhao

完整代码:

#include <iostream>
#include <queue>
using namespace std;

queue<string> V;
queue<string> N;

int main()
{
    int M;
    cin>>M;

    while(M--) //
    {
        string op,name,type;
        cin>>op;
        if(op=="IN")
        {
            cin>>name>>type;

            if(type=="V")
                V.push(name);

            else
                N.push(name);
        }
        else
        {
            cin>>type;

            if(type=="V")
                V.pop();
            else
                N.pop();

        }
    }

    while(V.size())
    {
        cout<<V.front()<<endl;
        V.pop();
    }
    while(N.size())
    {
        cout<<N.front()<<endl;
        N.pop();
    }
}

Map 映射

map 是一个关联容器,它提供一对一的 hash。

  • 第一个可以称为关键字(key),每个关键字只能在 map 中出现一次
  • 第二个可能称为该关键字的值(value)

map 以模板(泛型)方式实现,可以存储任意类型的数据,包括使用者自定义的数据类型。
Map 主要用于资料一对一映射(one-to-one)的情況,map 在 C++ 的內部的实现自建一颗红黑树,这颗树具有对数据自动排序的功能。在 map 内部所有的数据都是有序的。

比如,像是管理班级内的学生,Key 值为学号,Value 放其他信息的结构体或者类。

定义方式:

map<char, int> mymap1;
map<string, int> mymap2;

一般用法:
1.看容量

int map.size();//查询map中有多少对元素
bool empty();// 查询map是否为空

2.插入

    map.insert(make_pair(key,value));
    //或者
    map.insert(pair<char, int>(key, value))
    //或者
    map[key]=value

3.取值

map<int, string> map;

//如果map中没有关键字2233,使用[]取值会导致插入
//因此,下面语句不会报错,但会使得输出结果结果为空
cout<<map[2233]<<endl;

//但是使用使用at会进行关键字检查,因此下面语句会报错
map.at(2016) = "Bob";

4.遍历

map<string, string>::iterator it;
for (it = mapSet.begin(); it != mapSet.end(); ++it)
{
    cout << "key" << it->first << endl;
    cout << "value" << it->second << endl;
}

5.查找

m.count(key)//由于map不包含重复的key,因此m.count(key)取值为0,或者1,表示是否包含。
m.find(key)//返回迭代器,判断是否存在。

《弗里石的的语言》(上边出现过)

使用映射和字典解题,是的原来的代码减少了超过一半,但是思路还是一样,可以说是非常的巧妙且省力。

 #include <iostream>
 #include <map>
 using namespace std;

map<string,bool> mp;
int main ()
{

    int n;
    string ans="NO";
    cin>>n;
    for(int i=0;i<n;i++)
    {
        string word;
        cin>>word;
        if(mp.count(word)){
            ans=word;
            break;
        }

        else mp[word]=1;
    }
    cout<<ans<<endl;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

懒回顾,半缘君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值