[啃书] 第5篇

前言

第四章——算法初步

一些初级的/经典的算法思想:

排序/散列/递归/贪心/二分/two pointer;

尽量每一篇不要篇幅过长,所以本章大概分3~4篇来记录;

本篇总结自《算法笔记》4.1-4.2,主要记录排序和散列。

正文

知识点1:排序

冒泡排序已经在第二章讲过了 [传送门]

选择排序(选择排序有很多种,这里讲最基础常用的一种)

总体说:从待排序部分挑出最小的元素与待排序部分首位交换,并把首位加入已排序部分,重复n-1次。示意图如下

具体说(也可直接下面看代码)

比如5个数,一开始5个都属于待排序部分,选出最小的放到待排序部分的第1位(整体的第1位),然后第1位算作已排序部分,此时有4个属于待排序部分,1个属于已排序;1

再从待排序部分(4个)中选出最小的,放到待排序部分的第1位(整体的第2位),第1位进入已排序部分的末尾,此时有3个属于待排序部分,2个属于已排序;2

再从待排序部分(3个)中选出最小的,放到待排序部分的第1位(整体的第3位),第1位进入已排序部分的末尾,此时有2个属于待排序部分,3个属于已排序;3

再从待排序部分(2个)中选出最小的,放到待排序部分的第1位(整体的第4位),第1位进入已排序部分的末尾,此时有2个属于待排序部分,4个属于已排序;4

最后一个不用排了,肯定最小,共进行n-1次排序。

插入排序(插入排序也有很多种,这里先讲直接插入排序)

对比选择排序:

选择排序——待排序部分选最小放前面,累积出来已排序的部分。

插入排序——已排序部分选位置给待排序的数,待排序部分一个个插入到已排序部分中。

总体说:进行n-1趟操作(无视第一位),每趟操作都把待排序部分的第一个插入到已排序部分的正确位置。

具体说(也可直接下面看代码)

比如5个数,忽略第1个,因为1个元素必然是有序的,此时已排序1个,待排序4个,所以选择待排序中第一个,一般从后往前比较已排序部分第一个比他小的数,插在它后面。(为什么不是从前往后找第一个比它大的数插在前面?因为写代码方便)

插入第1个后,已排序已经有2个了,待排序有3个,继续取待排序3个中第一个,从已排序部分末尾往前比较到第一个比他小的数,插在这个比它小的数后面,

插入第2个后,已排序已经有3个了,待排序有2个,继续取待排序2个中第一个,从已排序部分末尾往前比较到第一个比他小的数,插在这个比它小的数后面,

插入第3个后,已排序已经有4个了,待排序有1个,取待排序1个,从已排序部分末尾往前比较到第一个比他小的数,插在这个比它小的数后面。

#include<cstdio>
int showArr(int A[], int n){
    for(int i = 0 ;i < n; i++){
        printf("%d ",A[i]);
    }
} 

/* 选择排序 */
int* selectSort(int A[], int n){
    for(int i = 0; i < n; i++){ // 进行n趟操作 
        int k = i;
        /* 选出待排序部分最小值 */
        for(int j = i; j < n; j++){
            if(A[j] < A[k]){
                k = j;
            }
        }
        /* 待排序部分 最小值与首位交换 */ 
        int temp = A[i]; 
        A[i] = A[k];
        A[k] = temp;
    }
    return A;
}

/* 插入排序 */ 
int* insertSort(int A[], int n){
    for(int i = 1; i < n; i++){ // 进行n-1趟排序 (数组下标为0~n-1) 
        /* 暂存A[i],j从i开始往前枚举(i是待排序的第一位) */
        int temp = A[i], j = i;
        /* 找到待插入位置j */ 
        while(j > 0 && temp < A[j-1]){ // 只要待插入数小于枚举数 
            A[j] = A[j-1]; //枚举数后移 
            j--;    
        }
        A[j] = temp;
    }
    return A; 
}

int main(){
    int x;
    int* result = &x; // 指针初始化 
    int maxn = 100;
    int a[maxn];
    while(scanf("%d", &maxn) != EOF){//先读入数组长度 
        for(int i = 0; i < maxn; i++){//再读入maxn个数 
            scanf("%d", &a[i]);
        }
        result = insertSort(a, maxn); // 排序 
        //result = selectSort(a, maxn); 
        showArr(result, maxn); // 输出结果 
        printf("
");
    }
    return 0;
} 

以下方法可以获取数组A的长度,但作为参数传给函数后该方法失效 [原因], 所以要把数组长度作为参数传给函数。

int n = sizeof(A) / sizeof(A[0]); // 获取数组长度(数组传参后获取不到)

知识点2:排序题常用的sort函数

C++自带的sort函数

上机的时候大多只要得到排序的结果对排序算法无特殊要求,可以直接使用C++的sort函数(C中的qsort函数也可以但是要了解指针且不够sort简洁)[sort函数见第6章]

1)结构体排序

考试中通常不是简单的数字排序,而是附带很多信息的排序(为了方便把它们写到结构体后排序)

比如一个学生

struct Student{
    char name[10]; // 姓名
    char id[10]; // 准考证号
    int score; // 分数
    int r; // 排名
}stu[100010];

2)cmp函数编写

cmp函数提供sort函数的排序规则。

bool cmp(Student a, Student b){
  if(a.score != b.score) return a.score > b.score;
  else return strcmp(a.name, b.name) < 0
}
/*若成绩不等则成绩高的学生在前,若成绩相等则按姓名排序*/

注:其中strcmp函数是string.h头文件下的用来比较两字符串字典顺序大小的

strcmp的返回值不一定是-1或+1,不同编译器返回值不同

3)排名的实现

排完序后不能说完全实现了排名,还存在一个难点,即得分相同时,排名并列的情况。

解决方法:结构体定义时加上排名这一项

在数组排序完成后,用如下两种方法实现排名的计算:

stu[0].r = 1; // 先把数组第一个排名为1
for(int i = 1; i < n; i++){
  if(stu[i].score == stu[i-1].score) stu[i].r = stu[i-1].r; // 分数相等,排名并列
  else stu[i].r = i + 1; // 否则排名为当前下标+1
}
int r = 1;
for(int i = 0; i < n; i++){
  if(i > 0 && stu[i].score != stu[i-1].score) r = i + 1; // 直接输出r(题目不需要记录排名时)

}

sort函数实例【PAT A1025】 PAT Ranking

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

struct Student{
    char id[15]; // 准考证号 
    int    score; // 分数 
    int location_number; // 考场号 
    int local_rank; // 考场排名 
}stu[30010];

bool cmp(Student a, Student b){
    if(a.score != b.score) return a.score > b.score;
    else return strcmp(a.id, b.id) < 0;
} 

int main(){
    int n, k, num; // num为考生数
    scanf("%d", &n); // 输入考场数
    for(int i = 0; i < n; i++){
        scanf("%d", &k); // 输入第i考场人数 
        /*录入当前考场的考生信息*/
        for(int j = 0 ;j < k; j++){
            scanf("%s %d", stu[num].id, &stu[num].score); // 记录准考证和分数 
            stu[num].location_number = i; // 记录考场号 
            num++; // 总考生数+1 
        }
        /* 将该考场的考生排序 */ 
        sort(stu + num - k, stu + num, cmp);
        /* 该考场第1名的local_rank=1 */ 
        stu[num-k].local_rank = 1; 
        /* 考场内排名(以考场内第一名为标准) */ 
        for(int j = num - k + 1; j < num; j++){
            if(stu[j].score == stu[j-1].score){
                stu[j].local_rank = stu[j-1].local_rank; // 若与前者分数相等则local_rank也相等 
            }else{
                stu[j].local_rank = j + 1 - (num - k); // local_rank为本考场前面的人数 
            }
        } 
    } 
    printf("%d
", num); //输出总考生数 
    sort(stu, stu + num, cmp); // 总体排名 
    int r = 1; //暂存当前考生排名 
    /* 输出总排名 */
    for(int i = 0; i < num; i++){
        if(i > 0 && stu[i].score != stu[i-1].score){
            r = i + 1; //与前一个人分数不同时,排名+1 
        }
        printf("%s", stu[i].id);
        printf("%d %d %d
", r, stu[i].location_number, stu[i].local_rank);
    } 
    return 0;
}

知识点3:散列

散列(hash)是一种用空间换时间的方法,很常用。

例1:给出N个数,再给出M个数,问这M个数是否分别在N个数中出现过

最简单的思路:M个每个都遍历一轮N个数,时间复杂度达到O(MN),当M和N都很大(105级别)时,系统无法承受。

运用散列的思路:设定布尔型数组hashTable[100010],用来在开始录入N个数的时候记录某个数是否出现过,假设当前录入的数是x,则把hashTable[x]=true,即表示x在这N个数中出现过,反之没有出现过。

对M个欲查询的数则直接找hashTable对应的值是否为true即可。

如此的时间复杂度只有O(M+N)【N次记录+M次查找】

#include<cstdio>
const int maxn = 100010;
bool hashTable[maxn] = {false}; //初始化为false(只赋第一个)
int main(){
    int n,m,x;
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++){
        scanf("%d", &x);
        hashTable[x] = true; //数字x出现过 
    }
    for(int i = 0; i < m; i++){
        scanf("%d", &x);
        if(hashTable[x] == true){
            printf("YES
");
        }else{
            printf("NO
");
        }
    }
    return 0;
}

例1变形:给出N个数,再给出M个数,问这M个数分别在N个数中出现次数

只需把hashTable改成int型,录入N个数的时候对应的hashTable[x]++即可。

#include<cstdio>
const int maxn = 100010;
int hashTable[maxn] = {0};
int main(){
    int n,m,x;
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++){
        scanf("%d", &x);
        hashTable[x]++; //数字x出现次数增加 
    }
    for(int i = 0; i < m; i++){
        scanf("%d", &x);
        printf("%d
", hashTable[x]);
    }
    return 0;
}
例1变形

总结:直接把输入的数作为数组下标,来对这个数的性质进行统计(务必掌握,非常实用的做法!)

该方法用空间换时间,把时间复杂度降到O(1)

缺陷:若录入的数超过了105(如1111111111),甚至变成了字符串(如“I Love You”),就不能作为数组下标了。

解决方法:需要由散列(hash),把这些各种各样的元素都转为规定范围内的数字。

散列的作用:将元素通过一个函数转化为整数,使得该整数尽量唯一的代表这个整数。[其中的函数称为散列函数H,如元素转换前为key,转换后则为H(key)]

知识点4:对整数进行散列的方法

直接定址法(恒等/线性)[常用]

平方取中法(key的平方的中间若干位作为hash值)[很少用]

除留余数法 (H(key) = key % mod)[常用][存在冲突]

解决除留余数法中冲突的方法

线性探查法(Linear Probing): [该方法容易扎堆]

H(key)+1,即冲突了就找紧挨着的下一个位置,若超过表长则从表首元素开始

平方探查法(Quadratic Probing):[避免扎堆]

H(key)+12,H(key)-12,H(key)+22,H(key)-22,H(key)+32,按该顺序检查,若超过了表长则把H(key)+k2对表长取模作为下一个探查位。若出现H(key)-k2<0的情况,加表长的n倍得到第一个正数作为下一个探查位。

链地址法(拉链法)

把所有H(key)相同的key链成一条单链表,设定一个单链表数组Link[mod],其中Link[h]存放H(key)=h的的一条单链表,如此可以对应到所有的key。

补充:前两种都找到新的hash地址,称为开放定址法。

上机一般不需要自己实现,可直接使用标准模板库的map来直接使用hash功能解决冲突。(C++11后可以用更快的unordered_map)

知识点5:字符串的hash(初级)[进阶见12.1节]

若key不为整数,比如一个二维坐标映射为一个整数,假设坐标为(x,y) 其中x≥0, y≤Range

则令H(P) = x * Range + y,可每个坐标得到唯一H(P),再通过整数hash的方法得到更小的范围。

对于字符串,先假设字符串由26个大写字母A~Z构成,把它们视作0~25,接着相当于把二十六进制转为十进制,转换成的整数最大为26len-1,其中len为字符串长度

 对于有小写字母的,则把26~51代替小写字母,接着相当于五十二进制转为十进制,代码如下:

对于有数字的,52~61来代替数字,接着六十二进制转十进制

如果数字出现的有规律,比如在字符串末尾有规定个数的数字,如BCD4可先将BCD转为731,再拼接后面的数字得7314,代码如下:

综合问题:给出N个字符串(由3位大写字母组成),再给出M个查询字符串,问每个查询字符串在N个字符串中的出现次数。

#include<cstdio>
const int maxn = 100;
char S[maxn][5], temp[5];
int hashTable[26 * 26 * 26 + 10];
int hashFunc(char S[], int len){ //将字符串转换为整数 
    int id = 0;
    for(int i = 0; i < len; i++){
        id = id * 26 + (S[i] - 'A');
    }
    return id;
}
int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++){
        scanf("%s", S[i]);
        int id = hashFunc(S[i], 3); //将字符串S[i]转为整数
        hashTable[id]++; //该字符串出现次数+1 
    }
    for(int i = 0; i < m; i++){
        scanf("%s", temp);
        int id = hashFunc(temp, 3);    ///将字符串temp转为整数 
        printf("%d
", hashTable[id]); //输出该字符串出现次数 
    } 
    return 0;
}

本篇结束,下一篇是递归和贪心

加油

原文地址:https://www.cnblogs.com/cc1997/p/13095734.html