---
# System prepended metadata

title: HW2

---

# 2023 年「[資訊科技產業專案設計](https://hackmd.io/@sysprog/info2023/)」課程第 2 次作業
> 乙二 - Two
> 👒: interviewer
> ◼️: interviewee
> 
> 影片中戴帽子的是面試官，沒戴的是面試者

## 影片

- [第一題](https://youtu.be/KOVfvmkCNWE)

## [34. Find First and Last Position of Element in Sorted Array](https://leetcode.com/problems/find-first-and-last-position-of-element-in-sorted-array/)

👒: 乙二先生你好，歡迎來到本公司的面試。本公司主打的產品是音樂播放器，在年度回顧的時候會需要知道使用者今年第一次聽，以及最近一次聽某一首歌的時間。假設我們已經從資料庫中取出某位使用者今年的所有聽歌紀錄，你會怎麼找出這兩個時間？
◼️: 我想確認一下，我需要在某個使用者的所有聽歌紀錄中，找到第一次聽這首歌，以及最後一次聽的時間，對嗎？
👒: 是的。
◼️: 好，那麼聽歌紀錄的 schema 會有哪些欄位？
👒: 聽歌紀錄的 schema 會有使用者 ID、歌曲 ID、時間這三個欄位。
◼️: 那麼時間是使用 unix timestamp 儲存的嗎？
👒: 是的，你可以將它當作整數處理。
👒: 請問使用者與歌曲 ID 都是 serial int 嗎，我能不能將它們當成整數處理？
◼️: 是的，可以。
◼️: 請問資料會不會依照任何欄位排序過？
👒: 紀錄會根據時間，從小到大排序，但歌曲 ID 沒有排序。
◼️: 好，那請讓我舉個範例：如果使用者聽歌的 ID 順序分別為 `[1,2,3,1,2`]，那麼尋找第一次聽的紀錄是在 index=0，最後一次聽是在 index=3，之後再取出時間就好，請問我理解的正確嗎？
👒: 是的，這樣沒有錯。
◼️: 好，那我的方法會是這樣：寫一個回傳 int vector 的函式，接收 records 和 songId 兩個參數。使用兩個變數儲存第一次和最後一次聽那首歌的 index，分別從開始與結束向中間搜尋，如果找到了歌曲就紀錄並停止搜尋，最後回傳這兩個 index。
👒: 好，你可以開始實作了

```cpp
vector<int> findFirstAndLastListenIndexes(vector<Record>records, int songId){
    int firstIndex=-1;
    int lastIndex=-1;
    int currentIndex=0;
    while(currentIndex<records.size()){
        if(records[currentIndex].songId==songId){
            firstIndex=currentIndex;
            break;
        }
        currentIndex++;
    }
    
    currentIndex=records.size()-1;
    while(currentIndex>=0){
        if(records[currentIndex].songId==songId){
            lastIndex=currentIndex;
            break;
        }
        currentIndex--;
    }
    
    return  {firstIndex,lastIndex};
}
```

👒: 好，請你舉出一些範例來驗證這個演算法。
◼️: 如同我一開始舉的範例，如果聽歌的順序是 `[1,2,3,1,2]`，找歌曲編號 1，last  在第一個迴圈變成 0，而 lastIndex 則會變成 3。
◼️: 而如果是極端情況，例如空陣列，則會直接不進入迴圈；而找不到的情況則是迴圈完全跑完，但不改變 first 和 last index，都會回傳 -1。
👒: 好，這樣看起來每一種情況都有考慮到。
👒: 但是我注意到一個問題，在第一個迴圈找不到值的時候，這個演算法在第二個迴圈可能會做不必要的檢查，造成效能的浪費，你能修正這個問題嗎？
◼️: 當我們找不到第一個 index 的時候，就代表這首歌不存在於紀錄之中，如果是這樣的話，就不需要找最後一個 index，直接用 -1 就可以了。

```diff
vector<int> findFirstAndLastIndexes(vector<Record>records, int songId){
    int firstIndex=-1;
    int lastIndex=-1;
    int currentIndex=0;
    while(currentIndex<records.size()){
        if(records[currentIndex].songId==songId){
            firstIndex=currentIndex;
            break;
        }
        currentIndex++;
    }
    
+   if(firstIndex==-1){
+       return {-1,-1};
+   }
    
    currentIndex=records.size()-1;
    while(currentIndex>=0){
        if(records[currentIndex].songId==songId){
            lastIndex=currentIndex;
            break;
        }
        currentIndex--;
    }
    
    return  {firstIndex,lastIndex};
}
```

👒: 好，那麼可以請你分析這個演算法的時間複雜度嗎？
◼️: 這個演算法在 worst case 會需要搜索整個陣列一輪，所以時間複雜度是 O(n)。
👒: 那接下來的題目是假設聽歌紀錄已經根據歌曲 ID 排序過的情況，你會怎麼修改這個演算法來改善時間複雜度？
◼️: 請問除了依照歌曲 ID 排序，還會跟原本一樣保持以時間排序嗎？
👒: 是的，會和原本一樣升冪排序。
◼️: 好，那舉例來說，陣列的歌曲 ID 可能是 `[1,1,2,3,3]`，但不可能是`[1,3,2,2,1]`，請問這樣對嗎？
👒: 對。
◼️: 那舉例來說，如果歌曲 ID 是 `[1,1,2,3,3]` 找 1，那就應該回傳 0 和 1，對嗎？
👒: 對
◼️: 好，那麼我認為這題可以使用 binary search，因為陣列是升冪排序的。而在找到了目標以後，再從那個地方往前後找尋第一個與最後一個 index。
👒: 好，你可以開始實作了。

```cpp!
vector<int> findIndexes(vector<Record> records, int songId)
{
  int left = 0;
  int right = records.size() - 1;

  int first = -1;
  int last = -1;

  // O(log n)
  while (left <= right)
  {
    int mid = left + (right - left) / 2; // prevent overflow
    if (records[mid].songId == songId)
    {
      first = mid;
      last = mid;

      // worst case: loop every element O(n)
      while (first - 1 >= 0 && records[first - 1].songId == songId)
      {
        first--;
      }

      while (last + 1 < records.size() && records[last + 1].songId == songId)
      {
        last++;
      }
      break;
    }
    else if (records[mid].songId > songId)
    {
      right = mid - 1;
    }
    else
    {
      left = mid + 1;
    }
  }
  return {first, last}; // 0, 1
}
}

```

👒: 看起來沒有問題，你能舉出一些例子來證明演算法的正確性嗎？同時考慮極端情況。
◼️: 好，極端情況可能是空陣列或是找不到值，如果是空陣列，會直接不進入迴圈。
◼️: 而如果是找 5，會因為 `left > right` 而跳出迴圈，就會回傳 {-1,-1}。
◼️: 而如果沒有 overflow，則任何長度的陣列都是使用 binary search 找出值，並且向兩邊尋找。例如 `[1,1,2,3,3]` 找 1，mid=2、太大所以right=1, mid=0，找到，然後向左右找，找到 index 0 和 1。

👒: 好，這樣看起來程式是正確的。你能分析一下這個演算法的時間複雜度嗎？
◼️: 這個演算法使用了 binary search，在找到值的時候會花 $O(logn)$ 的時間，而向兩邊尋找的時間複雜度則是 O(n)，因為有可能整個陣列都是同一個值。所以整個演算法的時間複雜度是 O(n)。
👒: 沒錯。我還有一個問題，有一些情況下，你可以利用歌曲 ID 升冪排序的特性，在binary search 以前就知道歌曲是否可能存在在陣列中，請問你知道應該怎麼實作嗎？
◼️: 如果歌曲 ID 是升冪的，那最小的值就是第一個，而最大的就是最後一個，我可以檢查目標是否超出了範圍，如果是的話，就可以直接回傳 {-1,-1}。
👒: 很好，請你替演算法加上這個功能。

```diff!
vector<int> findIndexes(vector<Record> records, int songId){
+   if (records.size() > 0 && (records[0].songId > songId || records[records.size() - 1].songId < songId))
+   {
+     return {-1, -1};
+   }


    int firstIndex=-1;
    int lastIndex=-1;
    
    int left=0;
    int right=records.size()-1;
    ...
```

◼️: 我們一開始需要檢查 `records` 的長度，確保我們的 index 不會超出去，而如果要找的 songId 超出了最大與最小的範圍，我們就不需要檢查，可以直接回傳找不到。
👒: 好，我們今天的面試到此結束，謝謝你今天的參與。

## 初步檢討

###  interviewer

- 優點
    - 有多個 follow up、提出能讓 interviewee 改進的地方
- 可改進的地方
    - 詢問的問題不夠多種，基本上都是時間複雜度／快速處理特殊情況／驗證正確性

### interviewee

- 優點
    - 有對於 database 的 schema 對題目做出變化（例如：比較紀錄的 songId）
    - 有問清楚各種細節 
- 可改進的地方
    - 應該主動測試演算法的正確性（主動帶入範例）
    - 有些口頭解釋太過繁雜，應該變得和 hackmd 上寫得一樣簡潔


## 對其他同學的批評

- 哈囉-Hi
    有把範例用註解寫下來很好，但是寫程式的時候可能要注意 coding style 一致，學到自己可以先把範例寫在註解。
- 肥俠-Billy
    英文單字發音需要加強，但是範例的寫法很清楚、有連續的 follow up，更能看出面試者的實力。
- 六六-Leo
    面試者有把題目輸入的條件也記在文件上很好，但面試官問問題、面試者回答問題有一點太快跳到結論、filler word 有點太多了，我自己也可以注意這件事。
- 瑪奇朵-Macchiato
    面試官問的問題很好，包含面試者為什麼要用某個方法、不同情況對於時間複雜度的影響等。
- 抹茶-Matcha
    leetcode 原題直接寫在 Google Docs 上，可能會讓 interviewee 直接背題目通過，而面試者的舉例和分析都很詳細。
    
## 第四次作業

### 盲狗

- interviewer
    - 有適當引導面試者應該朝哪個方向優化
- interviewee
    - 要重複問題的時候，不需要說自己要重複一次，說自己要確認會比較自然
    - 在講解自己的方法時，有寫出遞迴關係式，能展現自己有理解這個題目，不過可以另外寫下 n=1 時，S(n)=1, n=2, S(n)=2，讓面試官知道自己也有考慮到 edge case

### 節瓜

- interviewer
    - 變化後的題目好像要素有點太多，可以去除一些資訊，變成：「有一場藝術品拍賣會，拍賣會上多出了很多複製品，而這些畫作的拍賣編號都相同，請問你應該如何找出哪些拍賣品有複製品。」
- interviewee
    - 確認問題條件時，可以問是否會有多個拍賣品同時有複製品（陣列中有多個重複）