1. Two Sum
Given an array of integers, find two numbers such that they add up to a specific target number.
The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.
You may assume that each input would have exactly one solution.
Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2
題目的意思是給你一個目標數(target),讓你從給定的數組中找出兩個數nums[x]和nums[y]相加等於target,並返回這兩個值的索引x+1和y+1(需要加1是因為題目要求索引不是以0開始的)。
這個題目的一個難點是target可以是0也可以是負數,這一點題目里沒說清楚,在測試用例里面會出現。因為追求速度,所以所有leetcode的代碼我都會用C寫。
因為第一次刷leetcode,沒啥經驗的我一上來就too young too simple的來了個全暴力的搜索法,先固定第一個數,然后從固定的數往后面搜索第二個數,這個原始的暴力破解法時間復雜度接近n2
1 int* twoSum(int* nums, int numsSize, int target) { 2 int *indices = (int*)malloc(2 * sizeof (int)); 3 for(int i=0; i<numsSize-1; i++) 4 { 5 for(int j=i+1; j<numsSize; j++) 6 { 7 if(nums[i] + nums[j] == target) 8 { 9 indices[0] = i; 10 indices[1] = j; 11 return indices; 12 } 13 } 14 } 15 return NULL; 16 }
Time Limit Exceeded 被leetcode無情的拒絕了
於是我開始了新的嘗試,最開始想到的是調整外存循環的步長,也就是每次固定兩個數據,第二個數據分別和固定的兩個數據相加然后和target進行比較,這樣做的目的主要是為了減少內存訪問的次數,以為第二個數據取一次可以使用兩次,時間復雜度依舊接近n2,應該是因為內存遍歷次數減少,所以有幸通過。代碼如下:
1 int* twoSum(int* nums, int numsSize, int target) { 2 int *indices = (int*)malloc(2 * sizeof (int)); 3 for(int i=0; i<numsSize-1; i+=2) 4 { 5 if(nums[i] + nums[i+1] == target) 6 { 7 indices[0] = i + 1; 8 indices[1] = i + 2; 9 return indices; 10 } 11 else 12 { 13 for(int j=i+2; j<numsSize; j++) 14 { 15 if(nums[i] + nums[j] == target) 16 { 17 indices[0] = i + 1; 18 indices[1] = j + 1; 19 return indices; 20 } 21 else if(nums[i + 1] + nums[j] == target) 22 { 23 indices[0] = i + 2; 24 indices[1] = j + 1; 25 return indices; 26 } 27 } 28 } 29 } 30 return NULL; 31 }
Runtime: 224 ms
這次很榮幸的通過了,不過速度實在是太慢,我嘗試過將外層循環的步長改為4,速度有提升,但是沒有太想大效果,最好的速度是194ms。
看到最好的4ms的速度,我依然不想就此罷手,於是我想到了hash,在使用hash的時候,內存的用法有很多比較不錯的改進,都會記錄在此。
1 #define LIST_LENGTH 100 2 3 typedef struct Item Item; 4 5 struct Item 6 { 7 int value; 8 int index; 9 Item * next; 10 }; 11 12 typedef struct List 13 { 14 Item *head; 15 Item *end; 16 } List; 17 18 void insertItem(List *list, Item *item) 19 { 20 int key = ( item->value / 2 ) % LIST_LENGTH; 21 22 if ( !list[key].head ) 23 { 24 list[key].head = item; 25 list[key].end = item; 26 } 27 else 28 { 29 list[key].end->next = item; 30 list[key].end = item; 31 } 32 33 } 34 35 int searchValue(List *list, int value) 36 { 37 int key = (value / 2) % LIST_LENGTH; 38 39 Item *p = list[key].head; 40 41 while(p) 42 { 43 if(value == p->value) 44 { 45 return p->index + 1; 46 } 47 p = p->next; 48 } 49 return 0; 50 } 51 52 void listFree(List *list) 53 { 54 for(int i=0; i<LIST_LENGTH; i++) 55 { 56 57 Item *p, *q; 58 p = list[i].head; 59 60 while(p) 61 { 62 q = p->next; 63 free(p); 64 p = q; 65 } 66 67 } 68 69 free(list); 70 } 71 72 int* twoSum(int* nums, int numsSize, int target) { 73 if(numsSize < 100) 74 { 75 return twoSumShort(nums, numsSize, target); 76 } 77 int *res = (int*)malloc(2 * sizeof(int)); 78 List *list = (List*)malloc(sizeof(List) * LIST_LENGTH); 79 memset(list, 0, sizeof(List*) * LIST_LENGTH); 80 for(int i=0; i<numsSize; i++) 81 { 82 Item *item = (Item*)malloc(sizeof(Item)); 83 item->value = nums[i]; 84 item->index = i; 85 item->next = NULL; 86 insertItem(list, item); 87 } 88 89 for(int i=0; i<target; i++) 90 { 91 int x = searchValue(list, i); 92 int y = searchValue(list, target - i); 93 94 if(x && y) 95 { 96 res[0] = x < y ? x : y; 97 res[1] = x + y - res[0]; 98 break; 99 } 100 } 101 listFree(list); 102 return res; 103 }
這個是使用hash的雛形,當然這份代碼里面存在很多問題,一下會一一解釋
問題1:為了減少搜索的次數,代碼line89~100嘗試將target分為[0, target], [1, target-1]...這些可能性,然后分別使用hash搜索前后連個值,但是這里漏掉的數組中有負數的可能性,比如說target = 0 = -10000 + 10000,所以不能以target作為搜索的支點。
問題2:在構建hash表的過程中,line80~87重復了numsSize次調用malloc為節點分配內存,因為malloc是很耗時的一個函數,所以這會導致函數運行起來非常慢。
問題3:因為動態分配內存了,所以引入了釋放內存的操作ListFree函數,這個是可以想辦法避免的
對照以上問題,經過多次嘗試后,終於出了一個最快版的,代碼如下:
1 #define LIST_LENGTH 1000 2 3 typedef struct Item Item; 4 5 struct Item 6 { 7 int value; 8 int index; 9 Item * next; 10 }; 11 12 typedef struct List 13 { 14 Item *head; 15 Item *end; 16 } List; 17 18 void insertItem(List *list, Item *item) 19 { 20 int key = (unsigned int)( item->value) % LIST_LENGTH; 21 if ( !list[key].head ) 22 { 23 list[key].head = item; 24 list[key].end = item; 25 } 26 else 27 { 28 list[key].end->next = item; 29 list[key].end = item; 30 } 31 32 } 33 34 int searchValue(List *list, int value) 35 { 36 int key = (unsigned int)(value) % LIST_LENGTH; 37 Item *p = list[key].head; 38 39 while(p) 40 { 41 if(value == p->value) 42 { 43 return p->index + 1; 44 } 45 p = p->next; 46 } 47 return 0; 48 } 49 50 // use hash 51 int* twoSum(int* nums, int numsSize, int target) { 52 int *res = (int*)malloc(2 * sizeof(int)); 53 List list[LIST_LENGTH]; 54 memset(list, 0, sizeof(List) * LIST_LENGTH); 55 Item item[numsSize]; 56 memset(item, 0, sizeof(Item) * numsSize); 57 58 for(int i=0; i<numsSize; i++) 59 { 60 item[i].value = nums[i]; 61 item[i].index = i; 62 insertItem(list, &item[i]); 63 } 64 65 for(int i=0; i<numsSize; i++) 66 { 67 int x = i + 1; 68 int y = searchValue(list, target - nums[i]); 69 70 if(x && y && (x ^ y)) 71 { 72 res[0] = x < y ? x : y; 73 res[1] = x + y - res[0]; 74 break; 75 } 76 } 77 78 return res; 79 }
Runtime: 4 ms
看這這個4ms,真是激動不已啊,終於站到最前面了,這份代碼對前面三個問題都進行處理。
問題1解決方法:
第一個數據通過nums數組來固定,通過hash來查找第二個相配的的數據 searchValue(list, target - nums[i]);如果不出現hash沖突,那麽此算法的時間復雜度為N,很好的解決了第一個問題
問題2解決辦法:對於通過for循環重復為數組中每個節點分配內存的做法可以通過一次性給所有節點分配好內存來改進 Item *item = (Item*)malloc(sizeof(Item) * numsSize); ,這樣先將所有的內存分配好,構建hash表的時候,直接通過給指針賦值,讓其指向對應的內存就行,這樣也方便也釋放內存。
問題3解決辦法:因為依舊使用了malloc分配堆內存,需要手動釋放,所有問題2的解決辦法依舊不是最好。因為只需要返回兩個index,所有最后選擇了使用棧內存(代碼line53~56),這樣速度夠快,也不需要手動釋放內存,因此,一個4ms的算法就此誕生啦!