前言
CS50是由美国哈佛大学和耶鲁大学联合开发的计算机基础公选课。作者是正在学习课程的初学者,特作此日志记录并总结学习上的收获。成果代码是通过CS50IDE的编译功能make
、执行功能./
和CS50课程配套练习的检验来完成的,因网络原因无法使用check50
或者提交功能进行更加专业的系统检验,所有代码均有可以探讨和指正的地方,还请经验丰富的先行者不吝赐教。
本日志可以作为学习交流的媒介,但如果您是和我一样的CS50课程初学者,该日志可能无法帮助到您,CS50的官网讲解和配套的Walkthrough会有更大的帮助。
启动这篇文章时没有学习完所有内容和填充所有课程的日志,因日志的个人性质,会陆续更新至学习结束。知识产权声明
本篇文章涉及到哈佛大学CS50课程的成果讨论,如侵犯到您的利益,请及时联系我做修改或删除的处理。
Week 0 Scratch
Week 1 C
Week 2 Arrays
Week 3 Algorithms
Lab 3
Sort
Problem Set 3
Plurality
概述
Plurality在Collins词典的翻译是
If a candidate, political party, or idea has the support of a plurality of people, they have more support than any other candidate, party, or idea.
简单来说意思就是在投票中以小服大的问题——在投票中得票数多的候选人(candidate)胜出。经过课程和指示学习,以下是成功运行的代码展示。
CS50课程作业已经给了学生distribution code,要求学生完成的部分仅为自定义功能函数bool vote(string name)
和void print_winner(void)
。
#include <cs50.h>
#include <stdio.h>
#include <string.h>
// Max number of candidates
#define MAX 9
// Candidates have name and vote count
typedef struct
{
string name;
int votes;
}
candidate;
// Array of candidates
candidate candidates[MAX];
// Number of candidates
int candidate_count;
// Function prototypes
bool vote(string name);
void print_winner(void);
int main(int argc, string argv[])
{
// Check for invalid usage
if (argc < 2)
{
printf("Usage: plurality [candidate ...]\n");
return 1;
}
// Populate array of candidates
candidate_count = argc - 1;
if (candidate_count > MAX)
{
printf("Maximum number of candidates is %i\n", MAX);
return 2;
}
for (int i = 0; i < candidate_count; i++)
{
candidates[i].name = argv[i + 1];
candidates[i].votes = 0;
}
int voter_count = get_int("Number of voters: ");
// Loop over all voters
for (int i = 0; i < voter_count; i++)
{
string name = get_string("Vote: ");
// Check for invalid vote
if (!vote(name))
{
printf("Invalid vote.\n");
}
}
// Display winner of election
print_winner();
}
// Update vote totals given a new vote
bool vote(string name)
{
// TODO
// If string name belongs to candidates[].name, implement candidates[].votes++
for (int i = 0; i < candidate_count; i++)
{
if (strcmp(candidates[i].name, name) == 0)
{
candidates[i].votes++;
return true;
}
}
return false;
}
// Print the winner (or winners) of the election
void print_winner(void)
{
// TODO
int most_votes = candidates[0].votes;
for (int i = 1; i < candidate_count; i++)
{
if (candidates[i].votes > most_votes)
{
most_votes = candidates[i].votes;
}
}
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes == most_votes)
{
printf("%s\n", candidates[i].name);
}
}
// Start loop
return;
}
vote(string name)
bool vote(string name)
{
// TODO
// If string name belongs to candidates[].name, implement candidates[].votes++
for (int i = 0; i < candidate_count; i++)
{
if (strcmp(candidates[i].name, name) == 0)
{
candidates[i].votes++;
return true;
}
}
return false;
}
print_winner(void)
void print_winner(void)
int most_votes = candidates[0].votes;
for (int i = 1; i < candidate_count; i++)
{
if (candidates[i].votes > most_votes)
{
most_votes = candidates[i].votes;
}
}
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes == most_votes)
{
printf("%s\n", candidates[i].name);
}
}
// Start loop
return; }
学习总结
在刚开始看代码时会感到很不适,在之前CS50的较为简单课程作业中,都是通过自己构建main
函数中的各种逻辑和运算,但Week 3作业要求的是完善自定义函数的各项功能。在看distribution code的时候一头雾水,对于函数所要求实现的功能云里雾里,甚至不知道应该怎么写,第一个vote(string name)
就折腾了很久,最后参考了外国网友的一串同样作业的代码才明白这只是一个简单的计票功能。
在做Plurality之前,离看完整节Week 3 Algorithm课程已经很久了,所以很多知识没有滚动起来,这样导致学习效率也会降低,由此可见计算机学习是一个连续的过程。在实现第二个函数print_winner(void)
就出现了知识脱节的问题,我总想着在第一个for
循环语句中就把优胜者或者平局者判断出来,但是我发现无论怎么调换和构思,对于我的学习程度来说都不太可能实现。在凌晨一点对着电脑抓狂。后来在一篇中文技术网站上看到有人提问,也是关于这个函数的问题,底下的评论让我恍然大悟:
如果有多个赢家,您不需要(也不应该)变量election_winner。
在第一个循环中,只确定most_votes的值。第二次遍历候选对象(从0开始,而不是像当前那样从1开始)。如果候选人的票数等于most_votes,那么你就知道这个候选人是胜利者,你可以把名字打印出来,或者其他需要的东西。
(来源https://www.5axxw.com/questions/content/n6zq0u)
我完全没必要,只需要先找到计票最大,再经历一次循环筛选出得票数为最大票数的候选人,最后用printf
函数输出即可。可能对于初学者,这都不算一个很难的逻辑问题。但是因为知识的脱节,和我本身脑力不足,我没有自己想到这样的方法。但是这个方法启发了我Pset 3 Runoff的理解。问题往往被无知的人复杂化,当我知道这个以后,可能会变得“有知”一点吧。
Runoff
概述
Runoff实现的功能是在一定程度上弥补Plurality选票制度的不足,比其更有公平性。Runoff通过每一个voter的投票偏好(perferences rank)进行多次淘汰。
其中要注意的是在CS50的课程中,简化了问题的复杂程度——即不会考虑偏好比候选人数少的情况。
完成代码展示
CS50课程作业已经给了学生distribution code,要求学生完成的部分仅为预定义功能函数vote(int voter, int rank, string name)
、tabulate(void)
、print_winner(void)
、find_min(void)
、is_tie(int min)
和eliminate(int min)
。
#include <cs50.h>
#include <stdio.h>
#include <string.h>
// Max voters and candidates
#define MAX_VOTERS 100
#define MAX_CANDIDATES 9
// preferences[i][j] is jth preference for voter i
int preferences[MAX_VOTERS][MAX_CANDIDATES];
// Candidates have name, vote count, eliminated status
typedef struct
{
string name;
int votes;
bool eliminated;
}
candidate;
// Array of candidates
candidate candidates[MAX_CANDIDATES];
// Numbers of voters and candidates
int voter_count;
int candidate_count;
// Function prototypes
bool vote(int voter, int rank, string name);
void tabulate(void);
bool print_winner(void);
int find_min(void);
bool is_tie(int min);
void eliminate(int min);
int main(int argc, string argv[])
{
// Check for invalid usage
if (argc < 2)
{
printf("Usage: runoff [candidate ...]\n");
return 1;
}
// Populate array of candidates
candidate_count = argc - 1;
if (candidate_count > MAX_CANDIDATES)
{
printf("Maximum number of candidates is %i\n", MAX_CANDIDATES);
return 2;
}
for (int i = 0; i < candidate_count; i++)
{
candidates[i].name = argv[i + 1];
candidates[i].votes = 0;
candidates[i].eliminated = false;
}
voter_count = get_int("Number of voters: ");
if (voter_count > MAX_VOTERS)
{
printf("Maximum number of voters is %i\n", MAX_VOTERS);
return 3;
}
// Keep querying for votes
for (int i = 0; i < voter_count; i++)
{
// Query for each rank
for (int j = 0; j < candidate_count; j++)
{
string name = get_string("Rank %i: ", j + 1);
// Record vote, unless it's invalid
if (!vote(i, j, name))
{
printf("Invalid vote.\n");
return 4;
}
}
printf("\n");
}
// Keep holding runoffs until winner exists
while (true)
{
// Calculate votes given remaining candidates
tabulate();
// Check if election has been won
bool won = print_winner();
if (won)
{
break;
}
// Eliminate last-place candidates
int min = find_min();
bool tie = is_tie(min);
// If tie, everyone wins
if (tie)
{
for (int i = 0; i < candidate_count; i++)
{
if (!candidates[i].eliminated)
{
printf("%s\n", candidates[i].name);
}
}
break;
}
// Eliminate anyone with minimum number of votes
eliminate(min);
// Reset vote counts back to zero
for (int i = 0; i < candidate_count; i++)
{
candidates[i].votes = 0;
}
}
return 0;
}
// Record preference if vote is valid
bool vote(int voter, int rank, string name)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (strcmp(candidates[i].name, name) == 0)
{
preferences[voter][rank] = i;
return true;
}
}
return false;
}
// Tabulate votes for non-eliminated candidates
void tabulate(void)
{
// TODO
for (int i = 0; i < voter_count; i++)
{
for (int j = 0; j < candidate_count; j++)
{
if (candidates[preferences[i][j]].eliminated == false)
{
candidates[preferences[i][j]].votes++;
break;
}
}
}
return;
}
// Print the winner of the election, if there is one
bool print_winner(void)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes > (voter_count / 2))
{
printf("%s\n", candidates[i].name);
return true;
}
}
return false;
}
// Return the minimum number of votes any remaining candidate has
int find_min(void)
{
// TODO
for (int i = 0, j = 0, min_votes = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
j++;
if (j == 1)
{
min_votes = candidates[i].votes;
}
if (candidates[i].votes <= min_votes)
{
min_votes = candidates[i].votes;
}
}
if (i == candidate_count - 1)
{
return min_votes;
}
}
return 0;
}
// Return true if the election is tied between all candidates, false otherwise
bool is_tie(int min)
{
// TODO
int j = 0;
int n = 0;
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
n++;
if (candidates[i].votes == min)
{
j++;
}
}
}
if (n == j)
{
return true;
}
return false;
}
// Eliminate the candidate (or candidates) in last place
void eliminate(int min)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
if (candidates[i].votes == min)
{
candidates[i].eliminated = true;
}
}
}
return;
}
学生完成部分
各个预定义函数为学生通过代码功能提示创作,Runoff中的自定义函数的功能和灵感是作者在CS50及其练习配套讲解Walkthrough的指导下完成的。
bool vote(int voter, int rank, string name)
首先了解到vote函数的类型是bool(布尔型变量),是逻辑型变量的定义符。就目前的学习深度而言,可以了解到vote函数返回的值是真或者假,即true或者false。
vote
函数需要实现的功能有二:
- 判断用户投票是否有效,即判断输入的名称是否在候选人中;
- 记录每一个(第
voter
个)投票人(voter)的投票中每一个喜好排名(rank
)所对应的名字(name
)。
实现代码如下。
bool vote(int voter, int rank, string name)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (strcmp(candidates[i].name, name) == 0)
{
preferences[voter][rank] = i;
return true;
}
}
return false;
}
值得注意的是
if (strcmp(candidates[i].name, name) == 0)
使用到了strcmp
,所以需要我们在头文件说明中添加#include <string.h>
进行调用。
void tabulate(void)
tabulate
函数的功能即为每一轮投票计数,在功能中使用了if (candidates[i].eliminated)
判断第i
个候选人(candidate
)是否参与计票,如果不计票,则将该投票人的这一票传递给该投票人的第二喜好。
实现代码如下。
void tabulate(void)
{
// TODO
for (int i = 0; i < voter_count; i++)
{
for (int j = 0; j < candidate_count; j++)
{
if (candidates[preferences[i][j]].eliminated == false)
{
candidates[preferences[i][j]].votes++;
break;
}
}
}
return;
}
新的学习:break
这次编程在代码中使用了break
,可以有效跳出当前循环体。在这里可以避免计票所有输入的偏好。
bool print_winner(void)
功能同函数类型及名称一样,返回真假并打印出胜选人。
实现代码如下。
bool print_winner(void)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes > (voter_count / 2))
{
printf("%s\n", candidates[i].name);
return true;
}
}
return false;
}
int find_min(void)
find_min(void)
需要找到当前轮次的最小值,并返回最小值。
实现代码如下。
int find_min(void)
{
// TODO
for (int i = 0, j = 0, min_votes = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
j++;
if (j == 1)
{
min_votes = candidates[i].votes;
}
if (candidates[i].votes <= min_votes)
{
min_votes = candidates[i].votes;
}
}
if (i == candidate_count - 1)
{
return min_votes;
}
}
return 0;
}
bool is_tie(int min)
bool is_tie(int min)
需要判断当前轮次的所有未被淘汰的候选人(candidates[i].eliminated = false
)得票数是否相同,即平局(tie)。
实现代码如下。
bool is_tie(int min)
{
// TODO
int j = 0;
int n = 0;
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
n++;
if (candidates[i].votes == min)
{
j++;
}
}
}
if (n == j)
{
return true;
}
return false;
}
void eliminate(int min)
void eliminate(int min)
解决的是淘汰问题,机制很简单,就是把当前轮次得票数最少的候选人的candidates[i].eliminated
的值由false
变为true
,实现代码如下。
void eliminate(int min)
{
// TODO
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].eliminated == false)
{
if (candidates[i].votes == min)
{
candidates[i].eliminated = true;
}
}
}
return;
}
学习总结
在经历Week 3 Pset中最简单的Plurality的洗礼之后,我是很有信心独自完成Runoff的,但是很遗憾,事情进展的没有想象中那么顺利。
在我阅读完CS50课程界面提供的关于Runoff的英文内容时,我想我大概理解到了其意思。我准备着手写第一个函数bool vote(int voter, int rank, string name)
时却无从下手,我回到distribution code的main
函数体中分析每一步的意义,但是毕竟是初学者,distribution code里面有很多我不熟悉的表达和技巧,看起来有点吃力。云里雾里地读完CS50给出的关于vote
函数所需要的实现的功能后,没有底气的把vote
函数完成,但现在看来根本不是那回事儿。
没有完全理解题意,结果是对着tabulate
函数冥想,从晚上的20:50想到图书馆闭馆,中途在看main
函数和尝试理解CS50给的英文释义之间反复横跳,我甚至用了翻译软件都无法完全透彻理解到函数所要表达的意思。我现在把当时困扰我的英文释义贴出来,估摸以后再来看帖的时候也未必能读懂。
The first call here is to a function called
tabulate
, which should look at all of the voters’ preferences and compute the current vote totals, by looking at each voter’s top choice candidate who hasn’t yet been eliminated.
该账日后慢慢算。
所以我很痛苦,一个晚上都没想出来,屁股都坐痛了。现在想想自己筛选信息的能力实在太弱,我大概是当时死扣英语字眼去了,忘了CS50提供Walkthrough这回事。回到寝室,整理完内务,微信上有朋友给我发了个微博链接让我点进去看Ajay II姐发的新视频。无奈手机Type-C转3.5mm接口不知何处,只能打开电脑接耳机看。
倒是没认真看,自己对于算法好像没有灵感,对于函数不知所云,计算机之路就此终结。心情大不好。好吧,就是这个时候想到了Walkthrough,看完17分钟的Brain Yu讲解,豁然开朗:功能搞清楚了,作业的目的也弄清楚了。
在日志中没有明确些功能到底是什么,但是和我一样在学习的同学,如果搞不清楚函数的目的,一定去看Walkthrough,B站上搜索“CS50 Walkthrough”就有。
而作业的目的不是让学生一定要读懂main
函数体在说什么,而是熟悉全局变量之后,熟练运用,然后实现功能。
当晚把voter
和tabulate
完成,第二天起来又陆陆续续修改,填补,把之后的函数也完成了。
在头天晚上写tabulate
的时候忽略了统计上的一个小问题,原来的函数会把每一个投票人的所有偏好都记录。虽然在CS50IDE中执行给的例子结果是对的,但是计票系统无法“及时止损”,代码就有问题,得修改。想到在学校内在修的“C程序设计”中讲了break
,恰好就用上了。用break
底气不太足,在百度上搜索了break
跳出的是条件语句,还是当前循环语句,或者是整个循环语句。break
跳出的是当前循环,我想如果不是这样,我可能会想起todo
。