經常使用數據結構

1 數組、字符串【Array、String】

1.1 字符串轉化

數組和字符串是最基本的數據結構,在不少編程語言中都有着十分類似的性質,而圍繞着它們的算法面試題也是最多的。html

不少時候,在分析字符串相關面試題的過程當中,咱們每每要針對字符串當中的每個字符進行分析和處理,甚至有時候咱們得先把給定的字符串轉換成字符數組以後再進行分析和處理。node

舉例:翻轉字符串「algorithm」。面試

解法:用兩個指針,一個指向字符串的第一個字符a,一個指向它的最後一個字符m,而後互相交換。交換以後,兩個指針向中央一步步地靠攏並相互交換字符,直到兩個指針相遇。這是一種比較快速和直觀的方法。...算法

//翻轉字符串「algorithm」
int main()
{
    char temp,a[] = "algorithm";
    int i,j,length = strlen(a);
    temp = NULL;
    i = 0;
    j = length - 1;
    while(i != j)
    {
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
        i++;
        j--;
    }
    for(i = 0;i < length;i++)
    {
        printf("%c\t",a[i]);
    }
    return 0;
}

1.2數組的優缺點  

數組的優勢在於:編程

  構建很是簡單數組

  能在O(1)的時間裏根據數組的下標(index)查詢某個元素數據結構

而數組的缺點在於:編程語言

  構建時必須分配一段連續的空間優化

  查詢某個元素是否存在時須要遍歷整個數組,耗費 O(n) 的時間(其中,n 是元素的個數)spa

  刪除和添加某個元素時,一樣須要耗費 O(n) 的時間

1.3【242】有效的字母異位詞

字母異位詞:

也就是兩個字符串中的相同字符的數量要對應相等。例如,s等於「anagram」,t等於「nagaram」,s和t就互爲字母異位詞。由於它們都包含有三個字符a,一個字符g,一個字符 m,一個字符 n,以及一個字符 r。而當 s 爲 「rat」,t 爲 「car」的時候,s 和 t 不互爲字母異位詞。

解題思路:

一個重要的前提「假設兩個字符串只包含小寫字母」,小寫字母一共也就26個,所以:

知識點:哈希映射

  能夠利用兩個長度都爲26的字符數組來統計每一個字符串中小寫字母出現的次數,而後再對比是否相等;

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int isAnagram(char * s, char * t)
{
    //判斷兩個字符串長度是否相等,不相等則直接返回 false
    if(strlen(s) != strlen(t))
        return 0;
    //若相等,則初始化 26 個字母哈希表,遍歷字符串 s 和 t
    int A[26] = {0},B[26] = {0};  //哈希映射
    int i;
    while(*s != '\0')
    {
        A[*s - 'a']++;
        B[*t - 'a']++;
        s++;
        t++;
    }
    //判斷兩個表是否相同
    for(i = 0; i < 26; i++)
    {
        if(A[i] != B[i])
            return 0;
    }
    return 1;
}
int main()
{
    char b[] = "nagaram",a[] = "anagram";
    if(isAnagram(a,b) != 0)
        printf("True");
    else
        printf("False");
    return 0;
}

  能夠只利用一個長度爲 26 的字符數組,將出如今字符串 s 裏的字符個數加 1,而出如今字符串 t 裏的字符個數減 1,最後判斷每一個小寫字母的個數是否都爲 0。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int isAnagram(char * s, char * t)
{
    //判斷兩個字符串長度是否相等,不相等則直接返回 false
    if(strlen(s) != strlen(t))
        return 0;
    //若相等,則初始化 26 個字母哈希表,遍歷字符串 s 和 t
    int A[26] = {0};  //哈希映射
    int i;
    while(*s != '\0\)
    {
        //s 負責在對應位置增長,t 負責在對應位置減小
        A[*s - 'a']++;
        A[*t - 'a']--;
        s++;
        t++;
    }
    //若是哈希表的值都爲 0,則兩者是字母異位詞
    for(i = 0; i < 26; i++)
    {
        if(A[i] != 0)
            return 0;
    }
    return 1;
}
int main()
{
    char b[] = "nagaram",a[] = "anagram";
    if(isAnagram(a,b) != 0)
        printf("True");
    else
        printf("False");
    return 0;
}

2 鏈表(LinkedList)

單鏈表:鏈表中的每一個元素其實是一個單獨的對象,而全部對象都經過每一個元素中的引用字段連接在一塊兒。

雙鏈表:與單鏈表不一樣的是,雙鏈表的每一個結點中都含有兩個引用字段。

2.1鏈表的優缺點

鏈表的優勢以下:

  鏈表能靈活地分配內存空間;

  能在O(1)時間內刪除或者添加元素,前提是該元素的前一個元素已知,固然也取決因而單鏈表仍是雙鏈表,在雙鏈表中,若是已知該元素的後一個元素,一樣能夠在 O(1) 時間內刪除或者添加該元素。

鏈表的缺點是:

  不像數組能經過下標迅速讀取元素,每次都要從鏈表頭開始一個一個讀取;

  查詢第 k 個元素須要 O(k) 時間。

2.2 應用場景

若是要解決的問題裏面須要不少快速查詢,鏈表可能並不適合;若是遇到的問題中,數據的元素個數不肯定,並且須要常常進行數據的添加和刪除,那麼鏈表會比較合適。而若是數據元素大小肯定,刪除插入的操做並

很少,那麼數組可能更適合。

2.3 經典解法

2.3.1 利用快慢指針(有時候須要用到三個指針)

典型題目例如:鏈表的翻轉,尋找倒數第k個元素,尋找鏈表中間位置的元素,判斷鏈表是否有環等等。

2.3.2 .構建一個虛假的鏈表頭

通常用在要返回新的鏈表的題目中,好比,給定兩個排好序的鏈表,要求將它們整合在一塊兒並排好序。又好比,將一個鏈表中的奇數和偶數按照原定的順序分開後從新組合成一個新的鏈表,鏈表的頭一半是奇數,後一半是偶數。

在這類問題裏,若是不用一個虛假的鏈表頭,那麼在建立新鏈表的第一個元素時,咱們都得要判斷一下鏈表的頭指針是否爲空,也就是要多寫一條ifelse語句。比較簡潔的寫法是建立一個空的鏈表頭,直接往其後面

添加元素便可,最後返回這個空的鏈表頭的下一個節點便可。

 

建議:在解決鏈表的題目時,能夠在紙上或者白板上畫出節點之間的相互關係,而後畫出修改的方法

2.4 【25】K個一組翻轉鏈表

解題思路:

參考:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/solution/kge-yi-zu-fan-zhuan-lian-biao-by-powcai/

這道題考察了兩個知識點:

  對鏈表翻轉算法是否熟悉

  對遞歸算法的理解是否清晰

在翻轉鏈表的時候,能夠藉助三個指針:prev、curr、next,分別表明前一個節點、當前節點和下一個節點,實現過程以下所示:

【遞歸】方法1:

  • 一、找到待翻轉的k個節點(注意:若剩餘數量小於k的話,則不須要反轉,所以直接返回待翻轉部分的頭結點便可)。
  • 二、對其進行翻轉。並返回翻轉後的頭結點(注意:翻轉爲左閉又開區間,因此本輪操做的尾結點其實就是下一輪操做的頭結點)。
  • 三、對下一輪k個節點也進行翻轉操做。
  • 四、將上一輪翻轉後的尾結點指向下一輪翻轉後的頭節點,即將每一輪翻轉的k的節點鏈接起來。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct slist
{
    int data;
    struct slist *next;
};
struct slist *reverse(struct slist *head,struct slist *tail)
{
    struct slist *pre = NULL;
    struct slist *next = NULL;
    while(head != tail)
    {
        next = head ->next;
        head ->next = pre;
        pre = head;
        head = next;
    }
    return pre;
}
struct slist *reverseKGroup(struct slist* head, int k)
{
    if(head == NULL || head ->next == NULL)
        return head;
    struct slist *newHead,*tail = head;
    int i;
    for(i = 0;i < k;i++)
    {
        //剩餘數量小於k的話,不須要反轉
        if(tail == NULL)
            return head;
        tail = tail ->next;
    }
    //反轉前K個元素
    newHead = reverse(head,tail);
    //下一輪的開始的地方就是tail
    head ->next = reverseKGroup(tail,k);

    return newHead;
}

void input(struct slist *head)
{
    struct slist *p = head ->next; //p是工做指針
    while(p != NULL)
    {
        printf("%d\t",p ->data);
        p = p ->next;
    }
}
void create(struct slist *head)
{
    //尾插法創建單鏈表
    struct slist *r,*temp; //r是尾指針,temp是臨時結點
    int i,x;
    r = head;
    printf("請輸入元素:\n");
    scanf("%d",&x);
    while(x != 9999)
    {
        temp = (struct slist *)malloc(sizeof(struct slist));
        temp ->data = x;
        temp ->next = r ->next;
        r ->next = temp;
        r = temp;
        scanf("%d",&x);
    }
}
int main()
{
    struct slist *head;//head是頭結點

    head = (struct slist *)malloc(sizeof(struct slist));
    head ->next = NULL;
    create(head);
    input(head);

    int k;
    printf("\n請輸入K:");
    scanf("%d",&k);
    head ->next= reverseKGroup(head ->next,k);
    input(head);

    return 0;
}

【遞歸】方法2:

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct slist
{
    int data;
    struct slist *next;
};
struct slist *reverseKGroup(struct slist* head, int k)
{
    struct slist *cur = head;
    int count = 0;
    // 找到待翻轉的k個節點
    while(cur != NULL && count != k)
    {
        cur = cur ->next;
        count++;
    }
    if(count == k)
    {
        cur = reverseKGroup(cur,k);
        while(count != 0)
        {
            count--;
            struct slist *tmp = head ->next;
            head ->next = cur;
            cur = head;
            head = tmp;
        }
        head = cur;
    }
    //若剩餘數量小於k的話,則不須要反轉,所以直接返回待翻轉部分的頭結點便可
    return head;  //head爲頭指針
}

void input(struct slist *head)
{
    struct slist *p = head ->next; //p是工做指針
    while(p != NULL)
    {
        printf("%d\t",p ->data);
        p = p ->next;
    }
}
void create(struct slist *head)
{
    //尾插法創建單鏈表
    struct slist *r,*temp; //r是尾指針,temp是臨時結點
    int i,x;
    r = head;
    printf("請輸入元素:\n");
    scanf("%d",&x);
    while(x != 9999)
    {
        temp = (struct slist *)malloc(sizeof(struct slist));
        temp ->data = x;
        temp ->next = r ->next;
        r ->next = temp;
        r = temp;
        scanf("%d",&x);
    }
}
int main()
{
    struct slist *head;//head是頭結點

    head = (struct slist *)malloc(sizeof(struct slist));
    head ->next = NULL;
    create(head);
    input(head);

    int k;
    printf("\n請輸入K:");
    scanf("%d",&k);
    head ->next= reverseKGroup(head ->next,k);
    input(head);

    return 0;
}

3 棧(Stack)

3.1 特色

棧的最大特色就是後進先出(LIFO)。對於棧中的數據來講,全部操做都是在棧的頂部完成的,只能夠查看棧頂部的元素,只可以向棧的頂部壓⼊數據,也只能從棧的頂部彈出數據。

3.2 實現

利用一個單鏈表來實現棧的數據結構。並且,由於咱們都只針對棧頂元素進行操做,因此借用單鏈表的頭就能讓全部棧的操做在O(1)的時間內完成。

3.3 應用場景

在解決某個問題的時候,只要求關心最近一次的操做,而且在操做完成了以後,須要向前查找到更前一次的操做。

若是打算用一個數組外加一個指針來實現類似的效果,那麼,一旦數組的長度發生了改變,哪怕只是在最後添加一個新的元素,時間複雜度都再也不是 O(1),並且,空間複雜度也得不到優化。

3.420】有效的括號

解題思路

利用一個棧,不斷地往裏壓左括號,一旦趕上了一個右括號,咱們就把棧頂的左括號彈出來,表示這是一個合法的組合,以此類推,直到最後判斷棧裏還有沒有左括號剩餘。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max 20
#define Stack char
int isValid(char * p)
{
    int len=strlen(p);

    if(len == 1)
        return 0;
    int top=1,i;
    Stack s[Max];  //堆棧存儲
    for(i=0; i<len; i++)
    {
        switch(p[i])
        {
        case '(':
        case '[':
        case '{':
            s[top++]=p[i];  //進棧
            break;
        case ')':
            if(s[top-1]=='(')
                top--;  //出棧
            else return 0;
            break;
        case ']':
            if(s[top-1]=='[')
                top--;  //出棧
            else return 0;
            break;
        case '}':
            if(s[top-1]=='{')
                top--;//出棧
            else return 0;
            break;
        }
    }
    if(top==0)
        return 1;  //輸出1表示匹配成功
    else
        return 0;   //輸出0表示匹配失敗
}
int main()
{
    char s[Max];
    printf("請輸入括號:");
    scanf("%s",s);

    if(isValid(s) != 0)
        printf("true");
    else
        printf("false");
    return 0;
}

3.5【739】每日溫度

方法一:從左到右依次遍歷  O(n^2)

針對每一個溫度值 向後進行依次搜索 ,找到比當前溫度更高的值,這是最容易想到的辦法。

其原理:從左到右除了最後一個數其餘全部的數都遍歷一次,最後一個數據對應的結果確定是 0,就不須要計算。

遍歷的時候,每一個數都去向後數,直到找到比它大的數,數的次數就是對應輸出的值。

#include <stdio.h>
#include <stdlib.h>

void dailyTemperatures(int* T, int TSize){
    int i,j,result[TSize];
    for(i = 0;i < TSize;i++)
    {
        int cur = T[i];
        if(cur < 100)
        {
            for(j = i+1;j < TSize;j++)
            {
                if(T[j] > cur)
                {
                    result[i] = j - i;
                    break;
                }
            }
            if(j == TSize)
                result[i] = 0;
        }
    }
    for(i = 0;i < TSize;i++)
    {
        printf("%d\t",result[i]);
    }
}
int main()
{
    int length,T[] = {73, 65, 85, 71, 69, 72, 76, 79};
    length = sizeof(T) / sizeof(T[0]);
    dailyTemperatures(T,length);
    return 0;
}
//LeetCode能夠經過,但超時,由於時間複雜度爲O(2^2)
int* dailyTemperatures(int* T, int TSize,int* returnSize){
    int i,j;
    int *result = malloc(sizeof(int)*TSize);  //動態數組
    *returnSize = TSize;
    for(i = 0;i < TSize;i++)
    {
        int cur = T[i];
        if(cur <= 100)
        {
            for(j = i+1;j < TSize;j++)
            {
                if(T[j] > cur)
                {
                    result[i] = j - i;
                    break;
                }
            } 
            //若以後再也不升高,則爲0
            if(j == TSize)
                result[i] = 0;
        }
    }
    return result;
}

方法二:從右到左依次遍歷

關鍵是要減小爲每一個數尋找值遍歷次數。以下圖所示,綠色部分區域會給屢次遍歷,若是咱們能減小這部分區域的遍歷次數,就能總體提升運算效率。

若是咱們先從計算右邊,那麼咱們計算過的位置就不須要重複計算,如圖所示:

 當前咱們須要計算 7575 位置,而後向右遍歷到 7171,由於咱們已經計算好了 7171 位置對應的值爲 22,那麼咱們就能夠直接跳 22 爲在進行比較,利用了已經有的結果,減小了遍歷的次數。

#include <stdio.h>
#include <stdlib.h>

void dailyTemperatures(int* T, int TSize)
{
    int i,j;
    int *result = malloc(sizeof(int)*TSize);  //動態數組
    //從右向左遍歷
    result[TSize - 1] = 0; //最後一個必定爲0
    for(i = TSize - 2; i >= 0; i--)
    {
        // j+= result[j]是利用已經有的結果進行跳躍
        for(j = i+1; j < TSize; j+=result[j])
        {
            if(T[j] > T[i])
            {
                result[i] = j - i;
                break;
            }
            //遇到0表示後面不會有更大的值,那固然當前值就應該也爲0
            else if(result[j] == 0)
            {
                result[i] = 0;
                break;
            }
        }
    }
    for(i = 0; i < TSize; i++)
    {
        printf("%d\t",result[i]);
    }
}
int main()
{
    int length,T[] = {73, 65, 85, 71, 69, 72, 76, 79};
    length = sizeof(T) / sizeof(T[0]);
    dailyTemperatures(T,length);
    return 0;
}

方法三:堆棧    O(n)

能夠運用一個堆棧 stack 來快速地知道須要通過多少天就能等到溫度升高。從頭至尾掃描一遍給定的數組 T,若是當天的溫度比堆棧 stack 頂端所記錄的那天溫度還要高,那麼就能獲得結果。

  • 對第一個溫度23度,堆棧爲空,把它的下標壓入堆棧;
  • 下一個溫度24度,高於23度高,所以23度溫度升高只需1天時間,把23度下標從堆棧裏彈出,把24度下標壓入;
  • 一樣,從24度只須要1天時間升高到25度;
  • 21度低於25度,直接把21度下標壓入堆棧;
  • 19度低於21度,壓入堆棧;
  • 22度高於19度,從19度升溫只需1天從 21 度升溫須要 2 天;
  • 因爲堆棧裏保存的是下標,能很快計算天數;
  • 22 度低於 25 度,意味着還沒有找到 25 度以後的升溫,直接把 22 度下標壓入堆棧頂端;
  • 後面的溫度與此同理。

該方法只須要對數組進行一次遍歷,每一個元素最多被壓入和彈出堆棧一次,算法複雜度是 O(n)。

 

 利用堆棧,還能夠解決以下常見問題:

  • 求解算術表達式的結果(LeetCode 22四、22七、77二、770)
  • 求解直方圖裏最大的矩形區域(LeetCode 84)
相關文章
相關標籤/搜索