1.【基礎題】–逆置/反轉單鏈表+查找單鏈表的倒數第k個節點,要求只能遍歷一次鏈表
2.【附加題】–實現一個Add函數,讓兩個數相加,但是不能使用+、-、*、/等四則運算符。ps:也不能用++、–等等
**
1,基礎題:
**
/*
注:本次代碼處理的是不帶環的單鏈表
*/
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode
{
int _val;
struct ListNode* _pNext;
}Node, *PNode;
/*
逆置 / 反轉單鏈表:
思路:可以利用三個指針來標記三個連續節點。
1,將頭結點的指針置爲NULL
2,將頭結點後面的_pNext在循環中改變指向:
第一個指針不變,逆置中間指針的指向,保留第三個指針的指向,作爲往後遍歷鏈表的路徑。
這樣遍歷鏈表,每次逆置中間節點的指向,通過第三個節點往後遍歷,將三個節點指針整體往後移一個節點。
最終改變整個鏈表。
通過以上思路,將鏈表分爲三種情況:
1,鏈表爲空。
2,鏈表只含有1個節點。
3,鏈表有2個及以上節點。
*/
PNode ReverseList(PNode pHead)
{
PNode pPre = NULL; //第一個指針,指向連續節點中的第一個節點,最終成爲新的頭結點指針。
PNode pCur = NULL; //第二個指針,指向連續節點中的第二個節點,修改_pNext指向的指針。
PNode pTail = NULL; //第三個指針,指向連續節點中的第三個節點,保持與單鏈表連接的指針。
//鏈表爲空的情況
if (NULL == pHead)
return NULL;
//鏈表只有一個節點的情況
if (NULL == pHead->_pNext)
return pHead;
//鏈表有2個及以上節點的情況
pPre = pHead;
pCur = pPre->_pNext;
pTail = pCur->_pNext;
//處理pHead的_pNext指向
pHead->_pNext = NULL;
//處理頭結點之後的節點_pNext指向
while (pCur)
{
pCur->_pNext = pPre;
pPre = pCur;
pCur = pTail;
if (pTail) //當該條件不成立時,表示pTail爲NULL,下一次循環中pCur也NULL,將會跳出循環。
pTail = pTail->_pNext;
}
//此時pPre指向的是新的頭結點
return pPre;
}
/*
查找單鏈表的倒數第k個節點,要求只能遍歷一次鏈表:
思路:通過兩個指針,一個前指針,一個後指針。
1,兩個指針,初始都是指向單鏈表的頭結點。
2,前指針先走K步。然後和後指針一起往後走。
3,直到前指針指向NULL。
注意,根據K的大小與單鏈表中元素個數的比較。可以分爲:
1,K的值大於單鏈表中元素個數。
2,K的值小於等於單鏈表中元素個數。
3,需要注意K爲0的情況
但是,鏈表中元素個數,必須通過遍歷之後,才能知曉。
所以,可以在前指針往後走K步的過程中(即K-1步及之前),同時判斷前指針是否爲NULL。
如果前指針爲NULL,表示單鏈表元素個數小於K。否則大於等於K。
*/
PNode Find_the_penultimate_K_node(PNode pHead, size_t k)
{
//使用左、右來標識前、後指針。pHead是否爲NULL,不影響程序邏輯。
PNode pLeft = pHead;
PNode pRigth = pHead;
if (!k)
return NULL;
//前指針先往後走K步
while (k && pLeft)
{
pLeft = pLeft->_pNext;
--k;
}
if (k) //k>0,pLeft=NULL.表示元素個數小於K
return NULL;
//走到這一步,說明單鏈表元素個數大於等於K,然後,前後指針開始一起往後走。直到前指針爲NULL
while (pLeft)
{
pLeft = pLeft->_pNext;
pRigth = pRigth->_pNext;
}
//返回倒數第K個節點指針。
return pRigth;
}
//打印單鏈表
void PrintList(PNode pHead)
{
while (pHead)
{
printf("%d->", pHead->_val);
pHead = pHead->_pNext;
}
printf("NULL\n");
}
int main()
{
int i = 0;
Node node[10] = {0}; //建立10個node用來做實驗
PNode pHead = &node[0]; //指向頭結點
PNode pTemp = NULL; //指向倒數第K個節點
int k = 0;
//給node賦值
for (; i < 10; ++i)
node[i]._val = i;
//構成一個單鏈表
for (i = 0; i < 9; ++i)
node[i]._pNext = &node[i + 1];
node[9]._pNext = NULL; //注意,最後一個節點指向NULL
PrintList(pHead); //打印舊的單鏈表
pTemp = Find_the_penultimate_K_node(pHead, 5); //尋找倒數第K個節點
printf("Find the penultimate K node is %d\n", pTemp->_val);
pHead = ReverseList(pHead); //逆置單鏈表
PrintList(pHead); //打印新的單鏈表
system("pause");
return 0;
}
**
2,附加題
**
(1)利用兩數相加,從後往前進位的思想
思路:
1,0和1可以表示某一位上的數值。(對於sum來說)
2,0和1還可以表示兩種狀態,即序列中某一位的狀態(不需要進位與需要進位)。(對於carry來說)
3,a^b運算的結果是:二進制序列,不進位,只相加的結果sum。(0^0=0,1^0=1,1^1=0)
4,a&b運算,然後左移一位,的結果是:二進制序列中哪些位是需要進位carry。(0&0=0,1&0=0,1&1=1,然後將序列左移1位)
5,通過sum與carry帶入3,4中的a與b,可以從後往前不斷進位,直到carry爲0,表示沒有需要進位的位。
/*
//遞歸版本
int Add_2(int a, int b)
{
if (0==b)
return a;
else
{
int sum = a ^ b; // 各位相加,不計進位
int carry = (a & b) << 1; // 記下進位
Add_2(sum, carry); // 求sum和carry的和
}
}
*/
/**/
//迭代版本
int Add_2(int a, int b)
{
while (b)
{
int sum = a ^ b;
int carry = (a & b) << 1;
a = sum;
b = carry;
}
return a;
}
(2)利用數組來計算。
這種辦法非常巧妙
思路:
1,數組通過下標的偏移,可以計算出基於首元素地址的某一元素位置,從而訪問該元素。
那麼我們可以利用編譯器的這種行爲,來讓編譯器自動幫我們進行加法運算。
2,32位下, int值與指針的sizeof大小都是4字節。char的大小爲1字節。
所以,可以將兩數a和b中。a強轉爲char*類型地址c。這樣,b用來表示下標,計算c[b]時,是以1字節爲單位計算偏移的。
*/
int Add(int a, int b)
{
char *c = (char *)a; //將a表示的值,看做是地址c。並且類型要是char*。因爲sizeof(char)=1
return (int)&c[b]; //&a[b]-->a+sizeof(char)*b表示地址。通過(int)強轉爲int值
}
由於這種方法是從別處瞭解,本人第一次見,所以需要仔細分析分析
1,只是用於32平臺?
是的,因爲通過int值與指針值在32位平臺下,都是4個字節,大小一樣。所以在解析時,可以通過(char *) 、(int)來相互強轉,而不會,多讀取或者少讀取字節數據。
2,b爲負數時,可滿足情況?
可以滿足。因爲,通過數組下標來訪問元素的本質,就是基於某個地址的偏移。可以有正偏移(往後偏移),也可以有負偏移(往前偏移)。
測試用例:
int a[5] = {1,2,3,4,5};
int* p = NULL;
p = &a[3];
printf("a[-1] = %d\n", p[-1]);//可以查看p[-1]的值是否爲3
3,a爲負數時,可滿足情況嗎?
可以滿足。因爲,在將a值轉化爲地址c時,會將a的補碼序列,看做無符號數。我擔心的是,在將有符號數化作無符號數,計算時,會不會出問題。
後來,終於想到,在兩個數計算時,其實是他們的補碼序列在計算。這樣就脫離了正負的約束,只是在讀取時,會根據是否有符號來決定是否判斷補碼的標識位。
所以,不管a的正負,甚至是b的正負,在計算a(有符號)+b(有符號)與計算c(無符號)+b(無符號)結果的二進制補碼序列相同。但是(int)會把無符號數強轉爲有符號的。這樣在讀取c+b結果表示的二進制補碼序列時,是以有符號數的規則來讀取的,這與讀取a+b的結果相同。