[英]I can't get the right output that I want and the answer changes every time
所以我試圖為這個問題編碼:
是的,我必須使用數組,因為它是必需的。
考慮將存儲在兩個 n 元素數組 A 和 B 中的兩個 n 位二進制整數相加的問題。兩個整數的和應該以二進制形式存儲在 (n+1) 元素數組 C 中。 正式陳述問題並編寫將兩個整數相加的偽代碼。
我知道ans
數組在addd
函數的末尾包含正確的輸出。 但是,我無法輸出該答案。
下面是我的代碼。 請幫助我找出代碼中我出錯的地方,以及我可以做些什么來改變它以使其正常工作。 我將不勝感激。
#include <iostream>
using namespace std;
int * addd(int a[], int n1, int b[], int n2)
{
int s;
if(n1<n2) {s=n2+1;}
else {s=n1+1;}
int ans[s];
int i=n1-1, j=n2-1, k=s-1;
int carry=0;
while(i>=0 && j>=0 && k>0)
{
ans[k]=(a[i]+b[j]+carry)%2;
//cout<<k<<" "<<ans[k]<<endl;
carry=(a[i]+b[j]+carry)/2;
i--; j--; k--;
}
//cout<<"Carry "<<carry<<endl;
ans[0]=carry;
return ans;
}
int main(int argc, const char * argv[]) {
// insert code here...
int a[]={0,0,0,1,1,1};
int n1=sizeof(a)/sizeof(a[0]);
int b[]={1,0,1,1,0,1};
int n2=sizeof(b)/sizeof(b[0]);
int *p=addd(a,6,b,6);
// cout<<p[1]<<endl;
// cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<" "<<p[4]<<" "<<p[5]<<" "<<p[6]<<endl;
return 0;
}
返回ans
是返回指向局部變量的指針。 指針指向的對象在 then 函數返回后不再有效,因此嘗試讀取它會導致未定義的行為。
解決此問題的一種方法是將地址傳遞給數組以保存您的答案,並填充它,而不是使用 VLA(這是一個非標准的 C++ 擴展)。
VLA(可變長度數組)是一個從運行時計算值中獲取其大小的數組。 在你的情況下:
int s;
//... code that initializes s
int ans[s];
ans
是 VLA,因為您沒有使用常量來確定數組大小。 但是,這不是 C++ 語言的標准特性(它是 C 語言中的可選特性)。
您可以修改您的函數,以便調用者實際提供ans
。
int * addd(int a[], int n1, int b[], int n2, int ans[])
{
//...
然后調用者將負責傳入一個足夠大的數組來保存答案。
您的功能似乎也不完整。
while(i>=0 && j>=0 && k>0)
{
ans[k]=(a[i]+b[j]+carry)%2;
//cout<<k<<" "<<ans[k]<<endl;
carry=(a[i]+b[j]+carry)/2;
i--; j--; k--;
}
如果一個數組比另一個短,則較短數組的索引將首先達到0
。 然后,當相應的索引變為負數時,循環將停止,而不處理較長數組中的剩余項。 這實質上使ans
的相應條目未初始化。 讀取這些值會導致未定義的行為。
為了解決這個問題,您應該使用基於carry
的正確計算和較長數組中的剩余條目填充ans
的剩余條目。
上面提供的原始答案假設您被限制為僅對輸入和輸出使用 C 樣式數組,並且您想要一個可以讓您保持接近原始實現的答案。
下面是一個更面向 C++ 的解決方案,假設您仍然需要提供 C 數組作為輸入,否則沒有其他約束。
C 數組包裝器AC 陣列不提供您在使用 C++ 容器時可能習慣的便利。 為了獲得其中一些不錯的特性,您可以編寫一個適配器,使 C 數組的行為類似於 C++ 容器。
template <typename T, std::size_t N> struct c_array_ref { typedef T ARR_TYPE[N]; ARR_TYPE &arr_; typedef T * iterator; typedef std::reverse_iterator<T *> reverse_iterator; c_array_ref (T (&arr)[N]) : arr_(arr) {} std::size_t size () { return N; } T & operator [] (int i) { return arr_[i]; } operator ARR_TYPE & () { return arr_; } iterator begin () { return &arr_[0]; } iterator end () { return begin() + N; } reverse_iterator rbegin () { return reverse_iterator(end()); } reverse_iterator rend () { return reverse_iterator(begin()); } };
使用 C 數組引用
您可以通過引用傳入數組,而不是傳入兩個參數作為數組的信息,並使用模板參數推導來推導數組大小。
返回一個std::array
盡管您無法像在問題中嘗試的那樣返回本地 C 數組,但您可以返回一個包含在struct
或class
的數組。 這正是便利容器std::array
提供的。 當您使用 C 數組引用和模板參數推導來獲取數組大小時,您現在可以在編譯時計算std::array
對於返回值應該具有的正確數組大小。
if (N2 < N1) return addd(b, a);
標准化輸入
如果假設參數已按特定順序排列,則解決問題要容易得多。 如果你總是希望第二個參數是更大的數組,你可以通過一個簡單的遞歸調用來做到這一點。 這是完全安全的,因為我們知道遞歸最多發生一次。
if (N2 < N1) return addd(b, a);
使用 C++ 容器(或相似的適配器)
我們現在可以將我們的參數轉換為前面顯示的適配器,還可以創建一個std::array
來保存輸出。
std::transform(aa.rbegin(), aa.rend(), ans.rbegin(),
ans.rbegin(),
[](int a, int b) -> int { return a + b; });
如果可能,利用現有算法
為了解決您原始程序的缺點,您可以稍微調整您的實現以嘗試刪除特殊情況。 一種方法是存儲將較長數組添加到0
的結果並將其存儲到輸出中。 但是,這主要可以通過對std::copy
的簡單調用來完成。
ans[0] = 0; std::copy(bb.begin(), bb.end(), ans.begin() + 1);
由於我們知道輸入僅由1
和0
,因此我們可以計算從較短數組到較長數組的直接加法,而無需考慮進位(這將在下一步中解決)。 為了計算這個加法,我們應用std::transform
和一個 lambda 表達式。
std::transform(aa.rbegin(), aa.rend(), ans.rbegin(), ans.rbegin(), [](int a, int b) -> int { return a + b; });
最后,我們可以通過輸出數組來修復進位計算。 這樣做之后,我們准備返回結果。 返回是可能的,因為我們使用std::array
來表示答案。
for (auto i = ans.rbegin(); i != ans.rend()-1; ++i) { *(i+1) += *i / 2; *i %= 2; } return ans; }
一個更簡單的main
函數
我們現在只需要將兩個數組傳遞給addd
函數,因為模板類型推導會發現數組的大小。 此外,使用ostream_iterator
可以更輕松地處理輸出生成器。
int main(int, const char * []) { int a[]={1,0,0,0,1,1,1}; int b[]={1,0,1,1,0,1}; auto p=addd(a,b); std::copy(p.begin(), p.end(), std::ostream_iterator<int>(std::cout, " ")); return 0; }
using namespace std;
不要using namespace std;
. 當我在 Code Review Stack Exchange 中處於活動狀態時,我從一個常見問題的文件中粘貼了一個摘要,但我這里沒有。 相反,你應該只聲明你需要的符號,比如using std::cout;
int * addd(int a[], int n1, int b[], int n2)
int a[]
形式的參數非常奇怪。 這來自 C 並且實際上轉換為int* a
並且本身不傳遞數組。
輸入應該是const
。
名稱不清楚,但我猜n1
是數組的大小? 在標准指南中,您會看到強烈建議不要傳遞指針加長度。 標准指南庫提供了一個簡單的span
類型來代替。
並且長度應該是size_t
而不是int
。
根據描述,我認為每個元素都只有一位,對嗎? 那么為什么是int
類型的數組呢? 我會使用bool
或者int8_t
因為更容易使用。
你回什么? 如果a
和b
以及它們的長度是輸入,那么您返回指向開頭的指針的輸出在哪里? 這並沒有給出值語義,因為您正在返回一個指向必須存在於其他地方的東西的指針,那么它的生命周期是多少?
int s;
int ans[s];
return ans;
嗯,這是你的問題。 首先,聲明一個大小不是常量的數組甚至是不合法的。 (這是一個 gnu 擴展,它實現了 C 的 VLA 功能,但並非沒有問題,因為它破壞了 C++ 類型系統)無論如何,您都將返回一個指向本地數組第一個元素的指針,那么當函數執行時內存會發生什么回報? 繁榮。
int s;
否。在創建值時對其進行初始化。
if(n1<n2) {s=n2+1;}
else {s=n1+1;}
學習圖書館。 怎么樣:
const size_t s = 1+std::max(n1,n2);
然后獲取內存的便攜方式是:
std::vector<int> ans(s);
如果一個數組比另一個短,您的主要邏輯將不起作用。 較短的輸入應該表現得好像它有前導零匹配。 考慮將“獲取下一位”的問題抽象化,這樣您就不會重復處理每個輸入的代碼並造成不可讀的混亂。 你真的應該先學會使用集合和迭代器。
現在:
return ans;
會按預期工作,因為它是一個value 。 您只需要將該函數聲明為正確的類型。 所以只需使用auto
作為返回類型,它就知道了。
int n1=sizeof(a)/sizeof(a[0]);
嗚嗚嗚。
有一個標准函數可以給出內置原始數組的大小。 但實際上,這應該作為傳遞的一部分自動完成,而不是作為單獨的事情,如前所述。
int *p=addd(a,6,b,6);
你寫了6
而不是n1
等等。無論如何,通過之前的編輯,它變成:
using std::size;
const auto p = addd (a, size(a), b, size(b));
最后,關於:
cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<" "<<p[4]<<" "<<p[5]<<" "<<p[6]<<endl;
使用循環怎么樣?
for (auto val : p) cout << val;
cout << '\n';
哦,不要使用endl
。 無論如何自動刷新的 cout 不需要它,而且它很慢。 現代最佳實踐是使用 '\\n' 然后在需要時(例如,從不)顯式flush
。
我們來看看:
int ans[s];
除此之外,這甚至不是標准的一部分,並且編譯器可能會給您一些警告(請參閱鏈接),該命令在堆棧中分配臨時內存,該內存在函數退出時被釋放:這就是為什么您每次都會得到不同的結果,您正在讀取垃圾,即在此期間可能已被覆蓋的內存。 例如,您可以將其替換為
int* ans = new int[s];
不要忘記在使用完緩沖區后(在函數之外)釋放內存,以避免內存泄漏。
其他一些注意事項:
int s;
if(n1<n2) {s=n2+1;}
else {s=n1+1;}
這可以更優雅地寫為:
const int s = (n1 < n2) ? n2 + 1 : n1 + 1;
此外,實際的計算代碼是不精確的,因為如果 n1 不等於 n2,它會導致錯誤的結果:您需要進一步的代碼來完成對最長數組的剩余位的處理。 順便說一句,由於您定義 s 的方式,您不需要檢查 k > 0。 以下應該工作:
int i=n1-1, j=n2-1, k=s-1;
int carry=0;
while(i>=0 && j>=0)
{
ans[k]=(a[i]+b[j]+carry)%2;
carry=(a[i]+b[j]+carry)/2;
i--; j--; k--;
}
while(i>=0) {
ans[k]=(a[i]+carry)%2;
carry=(a[i]+carry)/2;
i--; k--;
}
while(j>=0) {
ans[k]=(b[j]+carry)%2;
carry=(b[j]+carry)/2;
j--; k--;
}
ans[0]=carry;
return ans;
}
如果我可以編輯一下......我認為這對於初學者來說是一個看似困難的問題,並且在任何嘗試編碼之前,如上所述應該在設計審查中標記問題。 它告訴你在 C++ 中做一些不好/典型/慣用/不合適的事情,並用妨礙實際開發邏輯的問題分散你的注意力。
考慮您編寫的核心算法(並且 Antonio 已更正):可以理解和討論這一點,而無需擔心 A 和 B 是如何實際傳入以供此代碼使用的,或者它究竟是什么類型的集合。 如果它們是std::vector
、 std::array
或原始 C 數組,則用法將相同。 同樣,如何從代碼中返回結果? 你在這里填充ans
,它是如何進入和/或退出代碼並返回到main
是不相關的。
原始 C 數組不是 C++ 中的一流對象,並且有關於如何將它們作為參數傳遞的特殊規則(從 C 繼承)。
返回更糟糕,返回動態大小的東西是C 和內存管理中的一個主要問題,這樣的內存管理是錯誤和安全漏洞的主要來源。 我們想要的是值語義。
其次,在 C++ 中使用數組和下標不是慣用的。 您使用迭代器並抽象集合的確切性質。 如果您有興趣編寫本身不處理內存管理的超高效后端代碼(它由處理所涉及的實際集合的其他代碼調用),它看起來像std::merge
,這是一個古老的函數可以追溯到 90 年代初。
template< class InputIt1, class InputIt2, class OutputIt >
OutputIt merge( InputIt1 first1, InputIt1 last1,
InputIt2 first2, InputIt2 last2,
OutputIt d_first );
您可以找到具有相似特征的其他人,它們將兩個不同的范圍用於輸入和輸出到第三個區域。 如果您完全像這樣編寫addp
,您可以使用硬編碼大小的原始 C 數組調用它:
int8_t A[] {0,0,0,1,1,1};
int8_t B[] {1,0,1,1,0,1};
int8_t C[ ??? ];
using std::begin; std::end;
addp (begin(A),end(A), begin(B), end(B), begin(C));
請注意,由調用者准備足夠大的輸出區域,並且沒有錯誤檢查。
但是,相同的代碼可以用於向量,甚至是不同容器類型的任意組合。 這可以通過傳遞插入迭代器來填充std::vector
作為結果。 但是在這個特定的算法中,這很困難,因為您是以相反的順序計算它的。
改進原始 C 數組的情況,您可以使用std::array
類,它是完全相同的數組,但沒有奇怪的傳遞/返回規則。 它實際上只是一個包裝結構中的原始 C 數組。 請參閱此文檔: https : //en.cppreference.com/w/cpp/container/array
所以你可以把它寫成:
using BBBNum1 = std::array<int8_t, 6>
BBBNum1 addp (const BBBNum1& A, const BBBNum1& B) { ... }
里面的代碼可以像你一樣使用A[i]
等,但它也可以通過A.size()
獲取大小。 這里的問題是輸入長度相同,輸出也相同(不是大 1)。 使用模板,它可以被編寫成使長度靈活,但仍然只在編譯時指定。
該vector
類似於一個數組,但具有運行時長度。 它是動態的,並且是您應該在 C++ 中訪問的首選集合。
using BBBNum2 = std::vector<int8_t>
BBBNum2 addp (const BBBNum2& A, const BBBNum2& B) { ... }
同樣,此函數中的代碼可以引用B[j]
等,並使用與array
集合完全相同的B.size()
。 但是現在,大小是一個運行時屬性,每個屬性都可以不同。
您可以像在我的第一篇文章中一樣,通過將大小作為構造函數參數來創建結果,然后您可以按值返回vector
。 請注意,如果您編寫以下內容,編譯器將有效地執行此操作並且實際上不必復制任何內容:
auto C = addp (A, B);
好的,現在這種干擾至少已經消除了,您可以擔心實際編寫實現了。 我希望您確信使用vector
而不是 C 原始數組不會影響您的問題邏輯,甚至不會影響使用下標的(可用)語法。 特別是由於問題涉及偽代碼,我將其對“數組”的使用解釋為“合適的可索引集合”而不是原始 C 數組類型。
一起處理 2 個序列並處理不同長度的問題實際上是一個通用的想法。 在 C++20 中,Range 庫可以快速完成這項工作。 舊的 3rd 方庫也存在,您可能會發現它叫做zip
或類似的東西。
但是,讓我們看看從頭開始編寫它。 您想從兩個輸入中一次讀取一個項目,但要巧妙地使其看起來長度相同。 你不想寫同樣的代碼 3 次,或者詳細說明 A 較短或 B 可能較短的情況……只需抽象出它們一起讀取的想法,如果用完它提供零.
這是它自己的一段代碼,可以應用於 A 和 B 兩次。
class backwards_bit_reader {
const BBBnum2& x;
size_t index;
public:
backwards_bit_reader(const BBBnum2& x) : x{x}, index{x.size()} {}
bool done() const { return index == 0; }
int8_t next()
{
if (done()) return 0; // keep reading infinite leading zeros
--index;
return x[index];
}
};
現在你可以寫一些類似的東西:
backwards_bit_reader A_in { A };
backwards_bit_reader B_in { B };
while (!A_in.done() && !B_in.done()) {
const a = A_in.next();
const b = B_in.next();
const c = a+b+carry;
carry = c/2; // update
C[--k]= c%2;
}
C[0]= carry; // the final bit, one longer than the input
它可以寫得更緊湊,但這很清楚。
問題是,編寫backwards_bit_reader
超出了您迄今為止所學的范圍? 您還可以如何將相同的邏輯應用於 A 和 B 而不復制這些語句?
您應該學會識別有時稱為“代碼異味”的東西。 多次重復相同的代碼塊,並重復相同的步驟而沒有改變但它應用於哪個變量,應該被視為丑陋和不可接受的。
如果它們的長度不同,您至少可以通過確保 B 總是較長的來減少案例。 如果不是這種情況,請通過交換 A 和 B 來執行此操作,作為初步步驟。 (實際上,很好地實施是另一個題外話)
但是邏輯仍然幾乎是重復的,因為您必須處理進位一直傳播到最后的可能性。 剛才你有 2 個副本而不是 3 個。
至少在外觀上,擴展較短的循環是編寫一個循環的唯一方法。
它被簡化到愚蠢的地步,但如果不是在基數 2 中完成而是使用更大的值,這實際上是在實現多精度算法,這是人們想要做的真實事情。 這就是為什么我將BBBNum
上面的類型命名為“Bad Binary Bignum”。
深入到實際的內存范圍並希望代碼快速和優化也是您有時想做的事情。 BigNum 就是一個例子; 你經常在字符串處理中看到這一點。 但是我們想要制作一個高效的后端,在不知道它是如何分配的情況下對內存進行操作,以及調用它的更高級別的包裝器。
例如:
void addp (const int8_t* a_begin, const int8_t* a_end,
const int8_t* b_begin, const int8_t* b_end,
int8_t* result_begin, int8_t* result_end);
將使用提供的范圍作為輸出,而不知道或關心它是如何分配的,並且只要它是連續的,就接受任何連續范圍的輸入而不關心使用什么類型的容器來管理它。 請注意,正如您在std::merge
示例中看到的,傳遞begin
和end
而不是begin
和size
更慣用。
但是你有輔助功能,如:
BBBNum2 addp (const BBBNum2& A, const BBBNum2& B)
{
BBBNum result (1+std::max(A.size(),B.size());
addp (A.data(), A.data()+A.size(), B.data(), B.data()+B.size(), C.data(), C.data()+C.size());
}
現在,臨時用戶可以使用向量和動態創建的結果來調用它,但它仍然可以用於調用數組、預分配的結果緩沖區等。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.