常見演算法及問題需注意的技巧與簡單實現

Perrysky 發表於 2020-09-23

1、合併兩個有序陣列A,B,並使合併後的陣列在A中。(假設A中有足夠的空間儲存兩個陣列中的元素)

void mergeSortedArray(int A[],int m, int B[],int n)
{
    int index = n + m;//從後向前貼上可以避免沒有判斷的元素被覆蓋
    
    while(m>0 && n>0){
        if(A[m-1] > B[n-1]){
            A[--index] = A[--m];
        }
        else{
            A[--index] = B[--n];
        }
    }
    while(n>0){
       A[--index] = B[--n];
    }
    while(m>0){
       A[--index] = A[--m];
    }
}

1-1 寫一個函式 void *memmove(void *dest, const void *src, size_t n) 將一個字串陣列移動到目的地址為起點的地址。

void *memmove(void *dest, const void *src, size_t n)
{
    char *p1 = dest;
    const char *p2 = src;
    //要考慮兩個地址是否會有記憶體重疊的部分,不考慮會造成未移動的資料被覆蓋而丟失
    if(p2 < p1)//若源地址比目的地址小;從源地址的最後逆序賦值可以避免未拷貝的資料被覆蓋
    {
        p2 += n;
        p1 += n;
        while(n-- != 0)
            *--p1 = *--p2;
    }
    else{
        while(n-- != 0)
            *p1++ = *p2++;
    }
    return p1;
}

v = *p++; // * 和 ++ 同級,自右向左結合 等價於 v = *(p++)

​ // 先取p所指目標變數的值,賦予變數v; 再對 p 對進行增1,即指標p指向下一個目標變數。

v = (*p)++; // 先將變數 *p 的值賦予v,再對變數 *p進行增1

2、連結串列要刪除當前所指的節點,而不是當前指標的下一個節點:

技巧是:把當前節點的值用next的值覆蓋,然後刪除next節點

​ 相當於刪除當前節點,實際是刪除了next節點而將next的值拿來覆蓋了當前節點的值。

3、埃拉托色尼(The Sieve of Eratosthenes) 質數篩選法(常考點)

​ 演算法的核心思想:先將範圍內的數都假設為質數,然後從最小的質數開始,將質數的倍數更改為非質數。

//假設求2-200之間的質數
#include<stdio.h>
#include<string.h>
#define n 200
int main()
{
    int flag[n+1];
    memset(flag,1,sizeof(flag));//定義在string.h中
    flag[0] = flag[1] = 0;
    for(int i=2; i<n; ++i)
    {
        if(flag[i]){
            for(int j=2; i*j<n; ++j){//將質數的倍數都置為0
                flag[i*j] = 0;
            }
        }
    }
    int cnt = 0;//記錄質數個數
    for(int i=2; i<n; ++i){
        if(flag[i]){
            ++cnt;
            printf("%d ",i);
        }
    }
    printf("\n 質數個數為:\n",cnt);
    return 0;
}

演算法的雙層迴圈處還可以優化以提高執行效率

//假設求2-200之間的質數
#include<stdio.h>
#include<string.h>
#define n 200
int main()
{
    int flag[n+1];
    memset(flag,1,sizeof(flag));//定義在string.h中
    flag[0] = flag[1] = 0;
    for(int i=2; i<sqrt(n); ++i)//外層迴圈結束條件到sqrt(n)就可以 判斷條件也可以寫為 i*i<n
    //一個合數n必有一個不大於sqrt(n)的正因子,故一個數若是沒有小於sqrt(n)的正因子,則說明它是一個素數
    {
        if(flag[i]){
            for(int j=i*i; j<n; j+=i){//將質數的倍數都置為0。內層迴圈從i*i開始即可,之前的已經判斷過
                flag[j] = 0;
            }
        }
    }
    int cnt = 0;//記錄質數個數
    for(int i=2; i<n; ++i){
        if(flag[i]){
            ++cnt;
            printf("%d ",i);
        }
    }
    printf("\n 質數個數為:\n",cnt);
    return 0;
}

4、輸入一行字串,統計其中包括多少單詞,單詞之間用空格分隔。

#include<stdio.h> 
#include<string.h> 
main() 
{  
    char str[200];//定義字元陣列,儲存字串  
    int i;  
    int space=0;//空格標誌,0 表示新空格,1 表示連續空格  
    int num=0;//單詞數量    
    printf("請輸入字串:");  
    gets(str);  
    if(str[0]==' ')//去掉第一行開頭的空格   
        space=1;  
    for(i=0;str[i]!='\0';i++)// 這個迴圈很關鍵,裡邊的判斷很巧妙    
    {   
        if(str[i]==' ')//處理連續空格的情況,當前字元為空格   
        {    
            if(space==0)//新空格    
            {     
                space=1;//表示連續空格     
                num=num+1;    
            }   
        }   
        else    
            space=0;// 新空格     
    }  
    if(space==0)//如果字串不以空格結束,則單詞數增 1 
        num=num+1;  
    printf("單詞總數為: %d\n",num);//輸出結果 
} 

程式分析:本題容易出錯的地方主要是對字串前後空格的判斷。

5、反轉一個單連結串列(reverse linked list)

遞迴法

struct ListNode {
    int val;
    struct ListNode *next;
};
 
//遞迴反轉整個連結串列
struct ListNode* reverse(struct ListNode* head) {
    if (head->next == null) return head;
    struct ListNode* last = reverse(head->next);
    head->next->next = head;//將下一個節點變成當前節點的前驅節點
    head->next = null;
    return last;
}

非遞迴法(迭代法)

typedef struct ListNode {
    int val;
    struct ListNode *next;
}ListNode;
ListNode reverseList(ListNode head)
{
    ListNode pre = NULL;
    while(head != NULL)
    {
        ListNode cur = head;
        head = head->next;
        cur->next = pre;
        pre = cur;
    }
    return pre;
}

6、編寫一個函式,利用遞迴方法找出一個陣列中的最大值和最小值,要求遞迴呼叫函式的格式如下:

MinMaxValue(arr,n,&max,&min),其中arr是給定的陣列,n是陣列的個數,max、min分別是最大值和最小值。

void MinMaxValue(int arr[],int n, int* max, int* min)//這裡的man和min都是出參
{
    if(n==1)
    {
        *max = arr[0];
        *min = arr[0];
    }
    else
    {
        int tmp_max = arr[0];//因為arr+1操作,這裡在每次遞迴中都是變化的
        int tmp_min = arr[0];
        MinMaxValue(arr+1, n-1, max, min);
        if(*max < tmp_max) *max = tmp_max;
        if(*min > tmp_min) *min = tmp_min;
    }
}

7、一個C語言程式的編譯到執行的過程

在這裡插入圖片描述

預處理:用於將所有的#include標頭檔案以及巨集定義替換成其真正的內容,預處理之後得到的仍然是文字檔案,但檔案體積會大很多。

編譯:這裡的編譯不是指程式從原始檔到二進位制程式的全部過程,而是指將經過預處理之後的程式轉換成特定彙編程式碼(assembly code)的過程。

彙編:將上一步的彙編程式碼轉換成機器碼(machine code),這一步產生的檔案叫做目標檔案,是二進位制格式。這一步會為每一個原始檔產生一個目標檔案。

連結:將多個目標文以及所需的**庫檔案(.so等)**連結成最終的可執行檔案(executable file)。

8、行內函數

呼叫行內函數時,編譯器首先檢查呼叫是否正確(型別安全檢查或者自動進行型別轉換)。

如果正確,則將行內函數的程式碼直接替換函式呼叫,並且用實參換形參,於是省去了函式呼叫的開銷

因此,內聯機制增加了空間開銷而節約了時間開銷。(空間換時間)

行內函數與用 #define 命令實現的帶參巨集定義有些相似,但不完全相同:

用行內函數可以達到用 #define 巨集置換的目的,但不會出現 帶參巨集定義 的副作用: 如自增運算時,容易出現錯誤,因為巨集是直接替換引數的,比如:

#define square(a) (a)*(a)
int  a = 1;
int re = square(a++);
// 可能 a = 3  re = 2
// 因為編譯器可能以不同的方式對錶達式((a++)*(a++)進行求值

慎用行內函數

1)使用行內函數可以節省執行時間,但卻增加了目標程式的長度

2)函式體內出現迴圈或遞迴等複雜的結構控制語句時,s不適合定義為行內函數

3)一個好的編譯器將會根據函式的函式體,自動取消不值得的內聯