跳到主要內容

LeetCode之旅——Two Sum,哈希表的應用

· 閱讀需 2 分鍾

今天做的題叫Two Sum,簡單題,給定一個整形數組和一個整數target,存在唯一的兩個成員相加等於target,要求返回這兩個成員的位置。

示例:

Given nums = [2, 7, 11, 15], target = 9,

Because nums[0] + nums[1] = 2 + 7 = 9,
return [0, 1].

解決方案

1. 暴力算法

最簡單的當然是暴力算法,一個嵌套的for循環搞定。標準答案如下:

public int[] twoSum(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] == target - nums[i]) {
return new int[] { i, j };
}
}
}
}

2. 哈希表

第一種方法的時間復雜度是O(n2),空間復雜度O(1)。另一個更快的解決方案是利用哈希表。遍歷數組並將成員其插入哈希表,再查詢[target - 成員]是否在表中(不能等於自身),如果在則結果已產生。至於是一次遍歷實現還是兩次遍歷個人認為區別不大。
標準答案如下:

public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (map.containsKey(complement) && map.get(complement) != i) {
return new int[] { i, map.get(complement) };
}
}
}

總結

哈希表最大的優勢在於查詢。理想的哈希表可以以O(1)的時間復雜度處理查詢。此題的微妙之處在於,需要將數組索引作為表的值而數組成員作為鍵。而思維慣性可能讓人一時轉不過彎。

哈希表雖然高效,但卻不是任何時候都適用。它的空間復雜度是O(n),但其常數因子並不小,可能花費甚至浪費很大的空間。另外衝突處理也會一定程度降低哈希表的性能。

因此哈希表並不總是比暴力解法好。如果你的空間足夠,數據量大,查詢頻度高,我認為使用哈希表是合理的選擇。

C++實現貪吃蛇

· 閱讀需 4 分鍾

注意,編譯前源文件字符編碼必須為GB2312/GBK。否則填充字符會出現異常。

編譯時需要指定按C++11標準編譯,為了支持結構體字面量的語法。
g++ -std=c++11 -o gluttonous-snake.exe ./source.cpp

代碼

#include <cstdlib>
#include <conio.h>
#include <deque>
#include <iostream>
#include <time.h>
#include <windows.h>
//定义一次步进
typedef struct {
//是否纵向移动
bool virticle = false;
//移动步长
int offset = 0;
}Step;
//定义'成长礼包'类型
enum GiftType {
//礼物,增加长度
GIFT_GIFT = FOREGROUND_GREEN,
//陷阱,减少长度
GIFT_TRAP = FOREGROUND_GREEN | FOREGROUND_BLUE,
//利剑,直接死亡
GIFT_SWORD = FOREGROUND_RED
};
//定义移动区域。窗口宽度应为width的两倍,因为ansi字符宽度仅高度的1/2。
#define FACTORY_WIDTH 64
#define FACTORY_HEIGHT 36

//定义蛇身颜色
#define SNAKE_BODY_COLOR (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
using namespace std;
//控制台输出句柄
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//蛇身数据容器。
deque<COORD> snakeBody;
//死亡标记
bool dead = false;
//'成长礼包'
COORD gift, trap, sword;


//判定点是否在蛇身上
bool inSnake(COORD point) {
if (snakeBody.size() == 0)
return false;
deque<COORD>::iterator iter = snakeBody.begin();
while (iter != snakeBody.end()) {
if (point.X == (*iter).X && point.Y == (*iter).Y)
return true;
++iter;
}
return false;
}
//生成'成长礼包'
COORD createGift(GiftType color) {
//统计递归调用次数
static int count = 0;
++count;
COORD point;
srand(rand());
point.X = rand() % FACTORY_WIDTH * 2;
srand(rand());
point.Y = rand() % FACTORY_HEIGHT;
if (inSnake(point)) {
return createGift(color);
}
else {
SetConsoleCursorPosition(hOutput, point);
SetConsoleTextAttribute(hOutput, color);
cout << "█";
SetConsoleTextAttribute(hOutput, SNAKE_BODY_COLOR);
dead = count > 12 ? true : false;
count = 0;
return point;
}//连礼包都没地方放了,死了算了
}
//初始化
void init() {
system("cls");
snakeBody.clear();
srand(time(NULL));
snakeBody.push_front({ 0, 0 });
snakeBody.push_front({ 2, 0 });
snakeBody.push_front({ 4, 0 });
deque<COORD>::iterator iter = snakeBody.begin();
while (iter != snakeBody.end()) {
SetConsoleCursorPosition(hOutput, *iter);
cout << "█";
++iter;
}
sword = createGift(GIFT_SWORD);
trap = createGift(GIFT_TRAP);
gift = createGift(GIFT_GIFT);
}
//步进
void stepOnece(Step step) {
COORD head = snakeBody.front();
(step.virticle ? head.Y : head.X) += step.offset;
//死亡
if (inSnake(head)
|| snakeBody.size() == 0
|| (head.X == sword.X && head.Y == sword.Y)
|| head.X < 0 || head.X >= FACTORY_WIDTH * 2
|| head.Y < 0 || head.Y >= FACTORY_HEIGHT) {
dead = true;
return;
}
snakeBody.push_front(head);
SetConsoleCursorPosition(hOutput, head);
cout << "█";

int count;
if (head.X == trap.X && head.Y == trap.Y) {
count = 2;
trap = createGift(GIFT_TRAP);
}//陷阱,变短
else if (head.X == gift.X && head.Y == gift.Y) {
count = 0;
gift = createGift(GIFT_GIFT);
}//礼物,增长
else
count = 1;
for (int i = 0; i < count; i++) {
COORD back = snakeBody.back();
SetConsoleCursorPosition(hOutput, back);
printf(" ");
snakeBody.pop_back();
if (snakeBody.size() == 0) {
dead = true;
break;
}
}
}
//控制循环
void gameLoop() {
Step curStep = { false, 2 };
Step step = curStep;
while (!dead) {
for (int i = 0; i < 200; i++) {
Sleep(1);
if (!_kbhit())
continue;
char cmd;
bool breakdown = true;
cmd = _getch();
switch (cmd) {
case 'w':
step.virticle = true;
step.offset = -1;
break;
case 's':
step.virticle = true;
step.offset = 1;
break;
case 'a':
step.virticle = false;
step.offset = -2;
break;
case 'd':
step.virticle = false;
step.offset = 2;
break;
default:
breakdown = false;
break;
}
//禁止当前方向反方向步进
if (step.virticle == curStep.virticle && step.offset * curStep.offset < 0)
continue;
else
curStep = step;
if (breakdown)
break;
}
stepOnece(curStep);
}
}
int main() {
//设置缓冲区大小
SMALL_RECT rect = { 0, 0, 10, 10 };
SetConsoleWindowInfo(hOutput, true, &rect);
SetConsoleScreenBufferSize(hOutput, { FACTORY_WIDTH * 2, FACTORY_HEIGHT });
//设置窗口大小
rect = { 0, 0, FACTORY_WIDTH * 2 - 1, FACTORY_HEIGHT - 1 };
SetConsoleWindowInfo(hOutput, true, &rect);
//设置窗口样式
SetConsoleTextAttribute(hOutput, SNAKE_BODY_COLOR);
CONSOLE_CURSOR_INFO cursorInfo = { 1, false };
SetConsoleCursorInfo(hOutput, &cursorInfo);
//规则介绍
cout << "使用W、S、A、D控制方向,吃到绿色礼包长度增加,吃到黄色礼包时长度减短,"\
"吃到红色礼包直接死亡。您也可以按CTRL + C退出游戏。按任意键开始游戏。" << endl;
system("pause");
//开始游戏
while (true) {
init();
gameLoop();
system("cls");
SetConsoleCursorPosition(hOutput, { 0, 0 });
printf("You has dead! Retry? (y/n)");
char cmd;
cin >> cmd;
if (cmd != 'y')
break;
else
dead = false;
}
}

逆波蘭法-算術表達式語法分析C++

· 閱讀需 3 分鍾

逆波蘭表達式的C++實現。算術表達式求值,支持加減乘除和冪運算,支持圓括號改變優先級。

//后缀表达式练习
//2018-09-20
#include <stack>
#include <iostream>
#include <cstdlib>
#include <string>
#include <math.h>
using namespace std;
//定义优先级
enum Priority {
PRI_IGNORE,//无效字符,忽略
PRI_NUMBER,//操作数
PRI_PLUS,//加减
PRI_DIVIDE,//乘除
PRI_POWER,//幂
PRI_LEFT_PAR,//左圆括号
PRI_RIGTHT_PAR,//右圆括号
};
//取操作符优先级
Priority getPriority(char symbol) {
if ((symbol >= '0' && symbol <= '9') || symbol == '.')
return PRI_NUMBER;
else if (symbol == '+' || symbol == '-')
return PRI_PLUS;
else if (symbol == '*' || symbol == '/')
return PRI_DIVIDE;
else if (symbol == '^')
return PRI_POWER;
else if (symbol == '(')
return PRI_LEFT_PAR;
else if (symbol == ')')
return PRI_RIGTHT_PAR;
else
return PRI_IGNORE;
}
//中缀表达式转后缀表达式
string toSufixExpression(string express) {
stack<char> operendStack;//操作符暂存栈
string output;//输出后缀表达式
//上一操作符的优先级,如果当前处理的操作符优先级低于它则前操作符出栈
Priority lastPriority = PRI_NUMBER;
string::iterator iter = express.begin();
for (; iter != express.end(); ++iter) {
Priority priority = getPriority(*iter);
switch (priority) {
case PRI_IGNORE:
default:
continue;
case PRI_NUMBER:
output.push_back(*iter);
break;
case PRI_PLUS:
case PRI_DIVIDE:
case PRI_POWER:
case PRI_LEFT_PAR:
//遇到符号向前追加间隔符
output.push_back(' ');
while (operendStack.size()) {
if (getPriority(operendStack.top()) < priority || operendStack.top() == '(')
break;
output.push_back(operendStack.top());
operendStack.pop();
}
lastPriority = priority;
operendStack.push(*iter);
break;
case PRI_RIGTHT_PAR:
while (operendStack.size()) {
if (operendStack.top() != '(') {
output.push_back(operendStack.top());
operendStack.pop();
}
else {
operendStack.pop();
break;
}
}
break;
}
}
//处理完毕,剩余操作符出栈
while (operendStack.size()) {
output.push_back(operendStack.top());
operendStack.pop();
}
return output;
}
//计算后缀表达式
double caculate(string express) {
stack<double> calcStack;//计算存储栈
string::iterator iter = express.begin();
while (iter != express.end()) {
//字符串转整形,并压入栈
if (*iter >= '0' && *iter <= '9') {
string number;
do {
number.push_back(*iter);
++iter;
} while ((*iter >= '0' && *iter <= '9') || *iter == '.');
calcStack.push(atoi(number.c_str()));
continue;
}
//间隔符
if (*iter == ' ') {
++iter;
continue;
}
//操作符,执行运算
if (calcStack.size() < 2)
return 0;
double operandOne = calcStack.top();
calcStack.pop();
double operandTwo = calcStack.top();
calcStack.pop();
switch (*iter++) {
case '+':
calcStack.push(operandTwo + operandOne);
break;
case '-':
calcStack.push(operandTwo - operandOne);
break;
case '*':
calcStack.push(operandTwo * operandOne);
break;
case '/':
calcStack.push(operandTwo / operandOne);
break;
case '^':
calcStack.push(pow(operandTwo, operandOne));
break;
default:
calcStack.push(operandTwo);
calcStack.push(operandOne);
break;
}
}
if (calcStack.size() == 1)
return calcStack.top();
return 0;
}

int main() {
cout << "Please enter an expression:" << endl;
string express;//表达式
getline(cin, express);
if (express.empty()) {
cout << "empty express.\n";
return 0;
}
string output = toSufixExpression(express);
cout << caculate(output) << endl;
system("pause");
return 0;
}

PHP學習-LoadXML與網頁格式錯亂

· 閱讀需 3 分鍾

用php寫後端動態生成網頁內容的時候,用到了DOMDocument類的操作。為了減少創建元素和文本節點的代碼(與效率無關),使用了loadXML()方法載入靜態的HTML文本(通過heredoc)。

$xml = new DOMDocument(); $xml->loadXML(<<<_HTML <div class="-article"> <div class="-article-title" onselectstart="return false;"></div> <hr/> <div class="-article-body"></div> <hr/> <div class="-article-extra"></div> <div class="-aborted"></div> <div class="-article-picture"></div> </div> _HTML ); ...... echo xml->saveXML();

沒錯,就是用loadXML()載入HTML,這樣做是因為saveHTML()的時候會輸出完整的HTML文檔(包含html和body元素)而不是我想要的文章部分,而saveXML()則只需要去除首行的文檔聲明即可。

這裡不談這樣的做法好與不好,只說說我遇到的問題。

遇到的問題是,網頁版面亂了。出現了塊級元素的堆疊,就是生成的元素後面的同級元素變成了它的子元素。感覺是標籤沒有閉合導致的,用瀏覽器看了生成的網頁代碼,終於找到了問題所在。

耗子屎在這一行:

<div class="-aborted"></div>

由於一些原因,生成元素中這塊被廢棄了,後面的代碼又很多地方使用了getElementsByTagName方法獲取指定元素,需要靠子元素的位置定位,所以不好直接刪除(可見裝載靜態的html文檔結構並不是個好點子),於是把這個<div>的類名設置為-aborted,然後統一處理。因為是廢棄元素,所以自然也不會為它生成內容了,最後saveXML輸出的文本中,將這個空元素<div class="-aborted"></div>轉化成了<div class="-aborted"/>。在html5中自閉合標籤是有嚴格控制的,只有特定的標籤才允許,因此<div/>並沒有被瀏覽器認為是一個閉合的標籤,然後和後面的div塊混亂了,才導致的這個問題。

解決辦法是,給heredoc中的廢棄元素的內容加個空格,xml封裝器就不會將它當作空元素了。不過最好還是全都動態生成元素,少生么蛾子。或者使用其他更好的辦法。

PHP-防止靜態資源被直接訪問

· 閱讀需 7 分鍾

用PHP寫後端,想要達到用戶登錄後才可以訪問一些圖片和視頻資源的效果,因此要阻止用戶直接輸入資源地址訪問資源。

找了一些資料,自己總結了幾種方法。

1.根據Referer頭——防盜鏈

瀏覽器在發起HTTP請求時一般都會一同發送Referer頭。Referer頭是用戶跳轉前的頁面,也就是通過哪個頁面發起的請求。通過禁止非法Referer頭的資源請求可以一定程度防止資源被非法訪問。一般這個都是在web服務器軟件上設置而不是後端處理。由於這個方法並不是很靠譜所以沒試過(畢竟請求頭是由請求方控制的),不過應付一般用戶足夠了,特別適合用來防止其他網站掛自己服務器的資源鏈接以轉移服務器負載,不過一般小網站用不到就是了。

2.通過復雜文件名

給資源文件賦以隨機的文件名,用數據庫記錄,然後定期或不定期更新文件名,用戶訪問頁面時後端php動態的查詢資源文件名。

這個方法還算可以,缺點是頻繁的數據庫連接將會增大服務器負載。如果服務器支持可以試試數據庫持久連接,不過需要注意持久連接的一些坑,不然可能造成連接鎖死之類的問題。

3.隱藏資源——將資源文件放在用戶無法訪問的目錄。

這樣做有兩種方案可以選擇,一是在用戶需要訪問時將資源文件復制到相應位置(可通過創建硬鏈接避免時間和空間浪費),二是將所有對資源的訪問重定位到一個文件,後端統一驗證身份後輸出文件內容。

第一種其實意義不大,因為總是要讓用戶訪問的,那只要有授權用戶在需要資源文件,你就得把資源放在那裡,然後就誰都可以訪問了,再然後發現問題回到方法2了——還是得改文件名。

第二種方法是比較靠譜的方法。比如我將資源訪問重定向到resource.php這個文件,然後驗證身份後根據GET請求參數去找用戶請求的文件,然後用readfile函數讀取並輸出文件就OK。用戶的看到的網頁源代碼將類似這樣:

<img src="http://127.0.0.1/resource.php?path=filename"/>

filename可以是真正的資源相對路徑,因為用戶反正是無法直接訪問的,暴露文件路徑反而可以省去查數據庫的消耗。需要注意的是,如果需要傳遞的文件路徑中包含特殊字符如“/”等需要轉義。可在後端統一由某個接口封裝,生成安全的資源鏈接,類似這樣:

<?php function getURL($path) { return 'http://127.0.0.1/resource.php?path=' . urlencode($path); } ?> <body> <img src="<?php echo getURL('picture/dog.jpg')?>"> </body>

然後resource.php中驗證用戶是否已授權,如果是且資源訪問合法,readfile('/resource_path/' . $_GET['path']),結束。

這應該是目前最靠譜的辦法,不過它也有缺陷。如果請求的資源是視頻這種比較大的文件,瀏覽器會一直等待資源接收完畢才顯示後面的內容,而不是頁面加載完再以流媒體的方式加載視頻資源,因此這個方法無法用於大文件資源。同時,測試發現,css中的url()資源不支持這樣的方式,因此背景圖片之類的資源也無法使用這種方法。

4.大雜儈——結合兩種方法

非常不幸,我要做的東西正好需要請求大量視頻資源,因此採用方法3中的第二類發現瀏覽器一直轉加載視頻,後面的評論等板塊得等視頻下載完成才加載,完全不能忍。於是想了半天,採取了折中的辦法:對於小圖片、少量圖片、文本資源採用方法3第二方案,也就是資源訪問重定向到resource.php統一處理;對於大圖片、視頻資源、大量圖片和css中的資源,則通過臨時硬鏈接的方式。

上面3中已經說了readfile()輸出的方法,下面說說硬鏈接的具體做法。

我們知道,php中有個session的概念,它為一個會話生成一個id,並在客戶端以cookie的形式保存,在服務器端創建一個唯一的session文件,用於保存與相關會話相關的數據,每次用戶請求中包含的cookie便告訴服務器當前會話的相關數據,比如是否已經登錄等。

這裡正利用了php的session機制。

我在網站目錄中創建了一個temp目錄,它包含一個空文件index.html以防止用戶直接訪問目錄看到目錄下的文件列表(也可在服務器端配置禁止對目錄的訪問),因此用戶可以訪問該目錄下的文件而無法得知它包含哪些子目錄或文件,這是前提。

當用戶頁面需要請求一個需要授權訪問的資源時,如果用戶是授權的(通過session機制判斷),後端生成相應資源一個非隨機的硬鏈接。它是這樣的一個硬鏈接:

  1. 它的父目錄是temp/当前会话session ID/
  2. 它的文件名由它的相對路徑通過哈希算法生成,再加上文件後綴

例如,如果我的資源目錄(禁止用戶訪問)是/resource_path,存在資源/resource_path/video/dog.mp4。網站目錄是/(對於用戶),包含temp子目錄。那麼我的頁面應該這樣寫:

<video src="<?php echo '/temp/' . session_id() . '/' . md5('video/dog.mp4') . '.mp4'?>"></video>

之前的getURL函數變成這樣:

function getURL($path, $flow = false) {
//非法资源
if(!file_exists('/resource_path/' . $path)) {
return '';
}
//小文件资源,采用资源重定向方案
if(!$flow) {
return '/resource.php?path=' . urlencode($path);
}
//获取后缀
preg_match('/\.[^\.\/]*$/', $path, $sufix);
$filename = '/temp/'. session_id() . '/' . md5($path) . $sufix[0];
//判断资源链接是否已创建,WWW_ROOT为网站根目录实际路径
if(file_exists(WWW_ROOT . $filename)) {
return $filename;
}
//创建temp/sessionID目录
else if(!file_exists(dirname(WWW_ROOT . $filename))) {
mkdir(dirname(WWW_ROOT . $filename));
}
//创建硬链接。注意Windows不支持PHP的link函数,看你服务器平台
if(stripos(PHP_OS, 'WIN') === false) {
link(HEVER_ROOT . $path, HEVER_ROOT . $filename);
}
else {
system('mklink /H "' . HEVER_ROOT . $filename . '" "' . HEVER_ROOT . $path . '"');
}
return HEVER_HOME . $filename;
}

當然,md5()的參數也可以另外的算,不一定是相對路徑。

這樣做的好處是,當用戶再次請求相同的資源時,只需確保相應的硬鏈接存在即可直接不管了,因此需要這個硬鏈接的文件名是非隨機的避免查詢數據庫。而且這樣多了一層session id阻隔,即使非法用戶知道了生成資源鏈接文件名的規則也無法訪問到資源,因為資源鏈接是在當前會話的session id目錄下的,除非他能猜到某個會話的session id再模擬發送cookie——還不如讓他猜用戶名和密碼呢。

當然這個方法也不是完美的,由於可能會生成大量的硬鏈接和session文件並且廢棄後不會消失,需要定期執行清理腳本來清除過期的鏈接和文件。我的解決方案是,當用戶登錄或登出時觸發一個腳本,它會清除超過1天未訪問過的session文件和對應的temp目錄中的同名名錄(其實不是同名,session文件還有sess_前綴)。這個靈感來自於wordpress的偽cron機制。

需要注意的是,Windows系統不支持php的link函數,因此得用windows的shell。還有一點,清理php session文件時,如果php.ini中未配置session.save_path,在php中用session_save_path()可能獲取不到php默認的session文件存放目錄,因此建議在文件首主動配置session目錄,使用session_save_path(string $path)函數。

注:以上方案均未經過嚴密的測試,請謹慎參考。

求小於一個整數的質數的個數的n個版本

· 閱讀需 6 分鍾

輸入一個整數n,輸出不大於它的質數的個數。

這是一個經典的問題。不管用什麼算法,思路都是嵌套循環,對小於n的自然數判斷是否質數。

代碼不好貼,就截圖了。文末面有源碼鏈接。

基礎版

最笨的辦法就是按順序分別判斷:
//基础版
int primeNum_0(int n)
{
int div;//试除变量
int count = 0;//质数个数
for (int i = 2; i <= n; ++i)
{
for (div = 2; div < i; ++div)
{
if (i % div == 0)//不是质数
break;
}
if (div == i)//质数
++count;
}
return count;
}

測試輸出:

There are 9592 prime numbers in 99999.
Completed with 1429 ms.

升級版

//升级版
int primeNum_1(int n)
{
int div, count = 0;
for (int i = 2; i <= n; ++i)
{
//试除上限为i的平方根取较大整数
int top = floor(sqrt(i)) + 1;
for (div = 2; div < top; ++div)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

這裡對比上面的基礎版,盡管代碼有些變化,但可以看出它們僅有的區別就是當試除i的數div大於√i時,就不再繼續試除,而判定i為質數。

這裡利用了數本身的性質:如果一個數i可以被不小於√i的整數整除,那麼得到的商一定是不大於√i的整數。因此,在試除到√i時便可以判定i是否為質數了。

這樣一步操作,使運算的次數大大減少。

測試輸出:

There are 9676 prime numbers in 99999.
Completed with 24 ms.
There are 665107 prime numbers in 9999999.
Completed with 5680 ms.

對於輸入99999,對比基礎版的超過1秒的運算時間,升級版只用了不到0.1秒。

升級改良版

在升級版的基礎上,還可以改進。

//升级改良版
int primeNum_2(int n)
{
int div, count = 1;//2直接算作质数
for (int i = 3; i <= n; i = i + 2)
{
//因为试除从3开始,2的试除单独提出来
if(i % 2 == 0)
continue;
int top = floor(sqrt(i)) + 1;
for (div = 3; div < top; div = div + 2)
{
if (i % div == 0)
break;
}
if (div >= top)
++count;
}
return count;
}

這個版本相對於升級版又有了一些改動:試除從3開始,每次步進2。因為從循環裡是3開始的,所以對於2的試除單獨放出來(減少避免在循環中增加條件判斷),不影響性能。同時這樣3也無法正常計算了,索性把3也提前算上,count初始化為2。

這裡的原理也很簡單:任何大於2的偶數不可能是質數。為了應用這個原理,有了上面的改動,雖然代碼變得有些畸形,不過速度卻相對提升了接近一倍。

其實這裡還有個bug,當輸入1或者2的時候也會輸出有兩個質數。可以在循環外加上條件判斷,單獨處理,只是這樣代碼會不那麼美觀。事實上這個算法在設計上本身也很不完美,博主還沒有學過算法,很多不規範的地方請諒解。

測試輸出:

There are 9674 prime numbers in 99999.
Completed with 0 ms.
There are 665105 prime numbers in 9999999.
Completed with 2826 ms.

這裡99999的輸入已經可以在1毫秒之內解決了,9999999用了接近3秒,和升級版的5秒多相比有了不錯的提升。

從速度上來看,99999從20多毫秒提升到1毫秒,二十多倍,而9999999卻只是加快了兩倍左右,這或許和CPU的多任務機制有關,相關內容不怎麼熟悉,就不解釋了。因此用執行時間來反應算法速度並不很科學,或許用變量記錄最內層循環執行的次數會更好。

豪華版


//豪华版
int primeNum_3(int n)
{
int count = 1;
int maxSize;//最大存储质数的个数
//申请内存
if(MAX_SIZE < ceil(sqrt(n)))
maxSize = MAX_SIZE;
else
maxSize = (int)(sqrt(n));
int* primeNums = (int*)malloc((maxSize * sizeof(int)));

primeNums[0] = 2;//2先放进去
int size = 1;//当前存储的质数个数
int div, cur, top;
for (int i = 3; i <= n; ++i)
{
top = ceil(sqrt(i));//试除上限
cur = 0;//当前试除数在存储空间的位置
div = primeNums[cur];
while (i % div != 0)
{
if (div >= top)
{
if (size < maxSize)//判断存储空间是否已满
primeNums[size++] = i;//将质数加入存储数组
++count;
break;
}//找到质数

//若已试除到存储空间最后一个数,步进2
if (cur < size - 1)
div = primeNums[++cur];
else
div += 2;
}
}
free(primeNums);
return count;
}

還有一個可以利用的性質,對於正整數i,如果i 可以被div整除(div為小於i且大於2的非質數),那麼i一定存在小於div的質數可以整除i,因為非質數div可以被分為若幹質數的乘積。

而我們判斷一個數是否是質數是從2開始去試除這個數(即使這個判斷被單獨列出),而2是最小的質數,因此要判斷一個大於2的正整數i是否是質數,只需要判斷是否存在一個數k可以整除i,其中k是[2,√i]區間內的質數

因此,我們可以建立一個質數存儲表。每當判斷出一個數是質數時,就把這個數加入表中。因為我們是從小到大開始判斷,所以這個表也是從小到大的。然後對於一個數i,只需用這個表中不大於√i的質數去試除i,如果最後一不大於√i的質數都無法整除i,那麼i是一個質數。

這個方法比上一種快一些,但消耗的空間也大得多。

測試程序

int main()
{
int n = 0;
scanf("%d", &n);
if(n < 2)
return 0;

int result, timeCount;//计时

if(n <= 100000)
{
timeCount = clock();
result = primeNum_0(n);
timeCount = clock() - timeCount;
printf("%d 个质数,基础版,%d 毫秒\n", result, timeCount);
}//数值太大基础版耗时太久

timeCount = clock();
result = primeNum_1(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_2(n);
timeCount = clock() - timeCount;
printf("%d 个质数,升级改良版,%d 毫秒\n", result, timeCount);

timeCount = clock();
result = primeNum_3(n);
timeCount = clock() - timeCount;
printf("%d 个质数,豪华版,%d 毫秒\n", result, timeCount);

//system("pause");
}

總結

程序中還有一些考慮不周的地方甚至小bug,例如div的步進放在了跳出判斷的前面,這樣導致了奇質數的平方也被判斷為質數了。在代碼文件中修改了一些,可能仍然存在bug。

[轉載]gdb調試利器

· 閱讀需 7 分鍾

轉自:http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html

GDB是一個由GNU開源組織發布的、UNIX/LINUX操作系統下的、基於命令行的、功能強大的程序調試工具。 對於一名Linux下工作的c++程序員,gdb是必不可少的工具;

1. 啟動gdb

對C/C++程序的調試,需要在編譯前就加上-g選項:
$g++ -g hello.cpp -o hello
調試可執行文件:
$gdb <program>
program也就是你的執行文件,一般在當前目錄下。

調試core文件(core是程序非法執行後core dump後產生的文件):

$gdb <program> <core dump file> $gdb program core.11127
調試服務程序:
$gdb <program> <PID> $gdb hello 11127
如果你的程序是一個服務程序,那麼你可以指定這個服務程序運行時的進程ID。gdb會自動attach上去,並調試他。program應該在PATH環境變量中搜索得到。

2. gdb交互命令

啟動gdb後,進入到交互模式,通過以下命令完成對程序的調試;注意高頻使用的命令一般都會有縮寫,熟練使用這些縮寫命令能提高調試的效率;

運行

  • run:簡記為 r ,其作用是運行程序,當遇到斷點後,程序會在斷點處停止運行,等待用戶輸入下一步的命令。
  • continue (簡寫c ):繼續執行,到下一個斷點處(或運行結束)
  • next:(簡寫 n),單步跟蹤程序,當遇到函數調用時,也不進入此函數體;此命令同 step 的主要區別是,step 遇到用戶自定義的函數,將步進到函數中去運行,而 next 則直接調用函數,不會進入到函數體內。
  • step (簡寫s):單步調試如果有函數調用,則進入函數;與命令n不同,n是不進入調用的函數的
  • until:當你厭倦了在一個循環體內單步跟蹤時,這個命令可以運行程序直到退出循環體。
  • until+行號: 運行至某行,不僅僅用來跳出循環
  • finish: 運行程序,直到當前函數完成返回,並打印函數返回時的堆棧地址和返回值及參數值等信息。
  • call 函數(參數):調用程序中可見的函數,並傳遞“參數”,如:call gdb_test(55)
  • quit:簡記為 q ,退出gdb

設置斷點

  • break n (簡寫b n):在第n行處設置斷點
    (可以帶上代碼路徑和代碼名稱: b OAGUPDATE.cpp:578)
  • b fn1 if a>b:條件斷點設置
  • break func(break縮寫為b):在函數func()的入口處設置斷點,如:break cb_button
  • delete 斷點號n:刪除第n個斷點
  • disable 斷點號n:暫停第n個斷點
  • enable 斷點號n:開啟第n個斷點
  • clear 行號n:清除第n行的斷點
  • info b (info breakpoints) :顯示當前程序的斷點設置情況
  • delete breakpoints:清除所有斷點:

查看源代碼

  • list :簡記為 l ,其作用就是列出程序的源代碼,默認每次顯示10行。
  • list 行號:將顯示當前文件以“行號”為中心的前後10行代碼,如:list 12
  • list 函數名:將顯示“函數名”所在函數的源代碼,如:list main
  • list :不帶參數,將接著上一次 list 命令的,輸出下邊的內容。

打印表達式

  • print 表達式:簡記為 p ,其中“表達式”可以是任何當前正在被測試程序的有效表達式,比如當前正在調試C語言的程序,那麼“表達式”可以是任何C語言的有效表達式,包括數字,變量甚至是函數調用。
  • print a:將顯示整數 a 的值
  • print ++a:將把 a 中的值加1,並顯示出來
  • print name:將顯示字符串 name 的值
  • print gdb_test(22):將以整數22作為參數調用 gdb_test() 函數
  • print gdb_test(a):將以變量 a 作為參數調用 gdb_test() 函數
  • display 表達式:在單步運行時將非常有用,使用display命令設置一個表達式後,它將在每次單步進行指令後,緊接著輸出被設置的表達式及值。如: display a
  • watch 表達式:設置一個監視點,一旦被監視的“表達式”的值改變,gdb將強行終止正在被調試的程序。如: watch a
  • whatis :查詢變量或函數
  • info function: 查詢函數
  • 擴展info locals: 顯示當前堆棧頁的所有變量

查詢運行信息

  • where/bt :當前運行的堆棧列表;
  • bt backtrace 顯示當前調用堆棧
  • up/down 改變堆棧顯示的深度
  • set args 參數:指定運行時的參數
  • show args:查看設置好的參數
  • info program: 來查看程序的是否在運行,進程號,被暫停的原因。

分割窗口

  • layout:用於分割窗口,可以一邊查看代碼,一邊測試:
  • layout src:顯示源代碼窗口
  • layout asm:顯示反匯編窗口
  • layout regs:顯示源代碼/反匯編和CPU寄存器窗口
  • layout split:顯示源代碼和反匯編窗口
  • Ctrl + L:刷新窗口

注解

交互模式下直接回車的作用是重復上一指令,對於單步調試非常方便;

3. 更強大的工具

cgdb

cgdb可以看作gdb的界面增強版,用來替代gdb的 gdb -tui。cgdb主要功能是在調試時進行代碼的同步顯示,這無疑增加了調試的方便性,提高了調試效率。界面類似vi,符合unix/linux下開發人員習慣;如果熟悉gdb和vi,幾乎可以立即使用cgdb。

關於宏定義的問題

· 閱讀需 2 分鍾

C語言中的宏定義有時候很方便,有時候也有些不便。宏最重要的性質之一就是,它是在編譯的時候直接替換相應的關鍵字,只是簡單的替換。所以用宏定義表達式時需要額外注意。

今天打算寫一個函數庫,然後裡面的函數都是駝峰命名風格(例如funcMyFunction)的,想同時實現Pascal風格(FuncMyFunction)調用。顯然把代碼復制一遍定義新函數是很不明智的,體積增加,代碼大量重復,維護不便等等。

於是我就打算通過定義宏來實現:

namespace A
{
#define TestFunc testFunc
// ......
}

但問題就來了,宏定義是無視命名空間的。

也就是說,雖然是在名空間A中定義的,但它是全局有效的。如果實際項目中包含了這個頭文件,而這個項目又包含了其他庫的頭文件,萬一有相同名稱的函數就會出錯,因為所有文件中的"TestFunc"都會被替換為testFunc,然後出錯。雖然這個概率不大,但其實也不算太小。

想了半天也沒想到完美的解決方案,不過想到兩個規避的方法。

  1. 單獨用一個頭文件定義這些宏,然後在編譯時靈活決定是否包含它(或者在頭文件中設置條件編譯)。
  2. 給庫中的函數加上前綴,比如mylib_funcMyFunc,這樣再定義宏也能很大程度上規避重復,這條可以與第一條同時進行,應該比較保險了。

這個問題意義不是很大,不過想到了也就一說。