Let's Write_ 프론트엔드

순열 재귀 DFS + 백트래킹

카테고리 없음

https://leetcode.com/problems/permutations/

46. Permutations

 

## answer1  Memory: 64.3mb

/**
* @param {number[]} nums
* @return {number[][]}
*/

var permute = function (nums) {
let result = [];

function createElement(currentArray, left) {
if (left.length === 0) return result.push(currentArray);
left.forEach((n, i) =>
createElement(
[...currentArray, n],
[...left.slice(0, i), ...left.slice(i + 1)]
)
);
forEach 내부 동작 (재귀)
/*([], [1,2,3,4])
//n = 1, i=0
  createElement([1], [2,3,4])
//n'=2, i'=0
createElement([1,2],[3,4])
//n''=3, i''=0
createElement([1,2,3], [4])
//n'''=4, i''' = 0
createElement([1,2,3,4], [])
left === 0 , return;
//n''=4, i''=1
createElement([1,2,4], [3])
//n'''=3, i''' = 0
createElement([1,2,4,3], [])
left === 0 , return;
//n'=3, i'=1
createElement([1,3],[2,4])
//n''=2, i''=0
createElement([1,3,2],[4])
//n'''=4, i''' = 0
createElement([1,3,2,4], [])
left === 0 , return;
....
*/
}

createElement([], nums);

return result;
};

 

 

시간 복잡도 O(N∗N!)

n!: 가능한 순열의 개수

n: 하나의 순열이 n개의 숫자를 포함하므로 배열 복사 시 O(n) 추가됨

공간 복잡도 O(N∗N!)

 

 

| 방법                              | 시간 복잡도    | 공간 복잡도                    | 특징                             |

| ------------------------------- | --------- | ------------------------- | ------------------------------ |

| 📌 DFS + 백트래킹 (`dfs([], nums)`) | O(n × n!) | O(n) 스택 + O(n × n!) 결과 저장 | 간결, 직관적, 가장 널리 쓰임              |

| 📌 DFS + `used[]` 배열 (방문 여부 체크) | O(n × n!) | O(n) + O(n × n!)          | 배열 복사를 줄일 수 있음                 |

| 📌 Heap's Algorithm             | O(n × n!) | O(n × n!)                 | 재귀 없이 구현 가능. 배열 내부 스왑으로 메모리 절약 |

| 📌 BFS / Queue 기반               | O(n × n!) | O(n × n!)                 | 큐 사용. 구조는 복잡하지만 반복문 기반         |

 

 

✅ 실전에서 가장 좋은 함수: DFS + 백트래킹 (slice로 분리)

👍 장점

코드가 짧고, 읽기 쉽고, 직관적.

 

재귀 스택 외엔 특별한 메모리를 쓰지 않음.

 

디버깅 및 로직 이해가 가장 쉬움.

 

👎 단점

slice()와 spread (...) 연산으로 인해 메모리 복사 비용 있음.

 

구조 분석

스택 기반 구조가 적절히 활용됨

재귀는 내부적으로 콜스택(stack) 을 활용함.

 

순열 생성은 깊이 우선 탐색(DFS)이기 때문에 스택과 궁합이 좋음.

 

명시적으로 스택을 구현하지 않아도 콜스택이 자동으로 관리해줌.

🔍 재귀의 단점은 없을까?

단점  대응 방법

콜스택 오버플로우 위험 (깊은 재귀)  순열은 n!이 커서 실제 문제에서는 n ≤ 10 정도로 제한됨 → 실질적 문제 없음

성능 최적화가 어렵다 Heap's Algorithm처럼 반복문 기반도 있지만, 일반적 상황에서는 필요 없음

✅ 결론

순열 문제에서는 재귀가 거의 항상 최적 선택입니다.

코드가 짧고 명확하며,

문제의 재귀적 성질과 구조적으로 딱 맞아떨어지고,

유지보수와 확장성에도 유리합니다.

*/

 

📌 스왑(Swap)은?

배열의 두 요소 값을 맞바꾸는 것

스왑은 메모리 복사 없이 배열을 바꾸는 가장 효율적인 방법이에요.

 

arr[0] <-> arr[2] 스왑

[arr[0], arr[2]] = [arr[2], arr[0]];

 

/** 동시할당

 *  배열 형태로 동시에 할당하는 이유

값이 덮어쓰기 되기 전에, 미리 복사해두고 바꿔야 하기 때문입니다.

덮어쓰기 문제를 회피하기 위한 안전한 스왑 방식입니다.

 

Destructuring Assignment는 내부적으로 오른쪽 값을 먼저 평가 → 임시 버퍼에 저장 → 왼쪽 변수에 순차적으로 할당합니다.

그래서 원래 값이 덮여도 영향받지 않음.

 * */

 

/* 📌 DFS (Depth-First Search, 깊이 우선 탐색)

트리나 그래프에서 루트(root)부터 한 갈래로 끝까지 내려가면서 탐색하는 방식.

한 가지 선택지를 끝까지 가보고,

더 이상 갈 곳이 없으면 되돌아와서(backtrack) 다른 선택지를 탐색하는 구조예요.

*/

 

/* 📌 백트래킹 (Backtracking)

탐색 도중 "이 선택은 정답이 될 수 없어"라고 판단되면, 직전 단계로 돌아가서 다른 선택지를 탐색하는 기법이에요.

DFS를 할 때, 잘못된 경로는 빨리 포기하고 돌아가는 것.

그래서 DFS + 백트래킹은 모든 경우를 다 보는 게 아니라, 일부 경우는 가지치기해서 속도를 높이기도 해요.

*/

 

/* 함수 slice, splice 차이

| 구분    | `slice(start, end)`     | `splice(start, deleteCount)` |

| ----- | ----------------------- | ---------------------------- |

| 원본 변경 | ❌ **변경 없음 (immutable)** | ✅ **원본 배열 변경 (mutable)**     |

| 반환값   | 잘라낸 **새 배열**            | 삭제된 요소로 구성된 **배열**           |

| 용도    | 복사해서 새로운 배열을 만들 때       | 배열의 일부를 제거/삽입할 때             |

| 성능    | 비교적 빠름 (메모리 복사)         | 느릴 수 있음 (원본 변경, 구조 순서 재조정)      |

 

 

✅ 순열 문제에서 splice() 사용의 문제점

만약 splice()를 원본 배열에 직접 쓰면, 다른 재귀 호출에서도 동일 배열이 영향을 받음.

이건 재귀 백트래킹에서는 치명적입니다.

 

 

*/

// ex

function dfs(picked, unpicked) {

  // ❌ splice는 원본 unpicked를 바꿈 → 다른 브랜치에서 오류 발생 가능

  const num = unpicked.splice(i, 1);

  dfs([...picked, num], unpicked);

}

//🎯 정리:

// 재귀 함수 내에서 배열 상태를 안전하게 유지하려면 immutable한 slice() 또는 spread 연산이 필수입니다.

 

/*📊 시간 복잡도

함수  시간 복잡도  설명

slice() O(k)  k: slice된 부분의 길이. 새 배열 생성

splice()  O(n)  삭제 후 배열 뒤 요소들 재배치 발생

 

slice()는 단순 복사 → 상대적으로 빠름.

splice()는 배열 내용이 바뀌면서 shift 발생 → 느림.

 

특히 splice(i, 1)은 i 이후 요소를 모두 한 칸 앞으로 당김 → O(n)

*/

 

/*✅ 실무적 권장사항

상황  추천 방법

안전하게 배열 일부 제거 : slice(0, i).concat(slice(i+1)) 또는 [...slice(0, i), ...slice(i+1)]

배열 변형을 직접 해야 할 때  : splice() (단, 반드시 복사본에만 적용!)

성능이 매우 중요한 경우 : in-place swap + DFS (예: Heap’s Algorithm)

*/

 

/*

🔚 결론

재귀적 순열 알고리즘에서는 slice()가 권장됩니다.

 

splice()는 원본을 파괴하기 때문에, 복사 없이 쓰면 큰 버그가 날 수 있어요.

 

성능도 slice()가 더 빠르고 안정적입니다.

 

다만 splice()는 "복사본에 대해 안전하게 쓸 때"만 쓰세요.

*/

 

# 공간복잡도

// ##answer2 Memory: 56mb

/**
* @param {number[]} nums
* @return {number[][]}
*/
var permute = function (nums) {
let res = [];
function gen(comb, nums) {
if (!nums.length) res.push(comb);
for (let i = 0; i < nums.length; ++i) {
gen(
[...comb, nums[i]],
nums.filter((_, ii) => ii != i)
);
}
}
gen([], nums);
return res;
};
 

/* answer1 vs answer2 메모리

## answer1

[…currentArray, n]는 새 배열 1개,

left.slice(0, i)가 새 배열 1개,

left.slice(i + 1)가 새 배열 1개,

이 두 개를 [ ... , ... ]로 다시 합쳐 또 하나의 새 배열이 생깁니다.

총 4개의 새 배열 인스턴스 생성.

 

slice + spread 방식이 filter 방식보다 훨씬 더 많은 배열을 찍어내기 때문에, 호출 횟수가 쌓일수록 메모리 사용량이 크게 늘어납니다.

 

## answer2

메모리 사용량이 낮은 answer2 구현은

[…comb, nums[i]]는 새로운 배열 1개,

nums.filter(...)도 새로운 배열 1개를 만듭니다.

총 2개의 새 배열 인스턴스 생성.

 

전역 결과 배열 하나만 사용

filter 한 번으로 배열을 복제

반환 값이 콜 스택에 남지 않음

 

 

## 반복문 vs. forEach

 

for (let i=0; …)

단순 인덱스 기반 루프 구조로, 콜백 오버헤드가 없습니다.

 

left.forEach((n,i) => …)

클로저 형태의 콜백 함수를 호출하므로, 내부적으로 더 많은 함수 객체가 생성·수명 관리됩니다.

 

함수 한 번 호출당 콜백 생성 비용이 추가되고, 무수히 많은 재귀 호출마다 이 콜백들이 살아 있어야 하니 메모리 사용이 더 높아집니다.

그러나

forEach와 전통적인 for 문 간의 메모리 사용량 차이는 배열 복제 방식에 비하면 거의 무시할 만한 수준입니다. forEach는 내부적으로 콜백 함수를 호출하면서 스택 프레임을 생성하지만, 이 오버헤드는 수천~수만 번의 배열 클론(예: slice + spread)보다 훨씬 작습니다. 실제 V8 엔진에서도 forEach와 for 루프 간에 메모리 사용량 차이는 거의 없다고 보고되고 있습니다

 

벤치마크를 보면, forEach는 약간의 함수 호출 오버헤드로 인해 성능이 미세하게 떨어질 수 있지만, 메모리 사용 면에서는 for 루프와 거의 동일한 수준을 유지합니다. 즉, 56 MB vs. 61 MB의 차이는 forEach vs. for 자체보다는, slice×2 + spread로 생성된 임시 배열 수가 주된 원인입니다

 

*/

* 메모리 피크(peak)란?*

메모리 피크(peak)란 프로그램이 실행되는 동안 한 시점에 사용된 메모리의 최고치를 가리키며, 흔히 ‘최대 메모리 사용량’이라고도 합니다.

# 정의 및 중요성

정의: 프로그램 시작부터 종료 시점까지 순간순간 사용된 힙(heap), 스택(stack), 그리고 런타임이 관리하는 기타 메모리 영역의 합 중 가장 높은 값을 의미합니다.

중요성: 메모리 피크를 알아야 OOM(Out-Of-Memory) 오류를 방지하고, 배포환경(컨테이너, 서버리스)의 메모리 한계를 적절히 설정할 수 있습니다.

# 메모리 피크 발생 지점

데이터 구조 생성: 대용량 배열, 객체, 버퍼를 한꺼번에 만들 때

재귀 호출: 깊은 재귀가 쌓여 스택 프레임과 임시 자료구조가 동시에 메모리에 존재할 때

GC 대기: 가비지 컬렉션 이전까지 해제되지 않은 객체들이 많은 경우

# 측정 방법

언어별 도구:

JavaScript (Node.js): process.memoryUsage().heapUsed

Python: tracemalloc 모듈

Java: -Xmx 설정과 jstat

외부 프로파일러: Valgrind Massif, VisualVM, Chrome DevTools 등으로 힙 스냅샷(snapshot) 분석

# 관리 전략

메모리 절약 기법:

인플레이스(in-place) 연산, 스트리밍(streaming) 처리

청크(chunk) 단위로 나눠 작업

GC 튜닝: 가비지 컬렉터 주기와 동작 방식을 조정

 

리소스 한계 설정: 컨테이너 메모리 리밋, 서버리스 타임아웃 환경 제약을 미리 계산하여 설정

코드리뷰 작성 가이드

TIL

## **우리는 왜 코드리뷰를 해야하나요?**

:fire: “소프트웨어를 유지보수하는 조직에서 코드 한 줄을 변경한다고 했을 때, 코드리뷰가 도입되기 전에는 그러한 변경의 55% 정도가 문제를 일으켰다.

그러나 리뷰 과정이 도입된 이후에는 그러한 변경의 2% 정도에서만 문제가 발생했다.”

- **버그의 조기 발견**(불필요한 시간과 비용을 절감)
- **개발 컨벤션 준수**(가독성과 유지보수 편의성 극대화)
- **중복 코드 방지 및 모듈의 재사용성 증대**
- **배움의 기회**(‘아, 이렇게 쉬운 방법이 있었구나?’)

## **우리의 코드 리뷰 규칙**

- 2일 이내에 리뷰를 완료하여 리뷰의 병목을 해소합니다. (피쳐 마감 일자에 따라 우선순위를 결정합니다.)
- 이슈가 있다면 페어프로그밍으로 같이 해결합니다.
- 각자의 개발스타일은 다릅니다. 기존에 자신의 스타일이 정답이라는 생각을 버리고, “나의 의견은 이러한데 당신은 어떻게 생각하시나요?”와 같이 의견을 묻는 것이 좋습니다.
- 리뷰어가 진행해주는 QA 중 이슈가 발생된다면 리뷰이는 코드 변경사항과 개발된 피쳐와 관련된 모든 사항에 대해 전체 테스트 및 확인을 다시 진행합니다. 놓친 부분이 한 군데 발생된다면 다른 곳도 꼼꼼하게 다시 한번 점검하는 것이 좋습니다.
- 공통 컴포넌트를 수정한다면 사용하고 있는 모든 부분을 체크하여 side effect 여부를 확인합니다. 
- 앞으로도 함께 만들어가는 문화이니 어떻게 진행할지 같이 고민해봐요 :slight_smile: 언제든 의견 주세요<span dir="">\~</span>

## 리뷰 체크리스트

:grinning: MR올리기 전 아래 내용을 확인해주세요.

* [ ] **merge할 브랜치의 위치를 확인**하셨나요?
* [ ] **코드의 맥락(CONTEXT)을 이해할 수 있는 설명을 추가하셨습니까?** (ex.로그인 시 발생하는 이메일 인증 오류에 대한 버그 수정입니다)
* [ ] **기능이 정상적으로 동작**하나요?
* [ ] 변수명 등을 **통일성 있게, 축약 또는 생략하지 않고** 작성했습니까?
* [ ] **개발 컨벤션에 준수**하여 작성했습니까?

:grin: MR 올린후

* [ ] **기존 코드와 충돌이 발생**하면 머지 버튼이 비활성화 되기도 합니다! 그 부분까지 체크하여 충돌을 해결해야 리뷰를 진행할 수 있습니다.

:robot: **프로젝트를 진행하면서 체크리스트는 더효율적인 코드 리뷰를 위해 수정될 수 있습니다.**

## 리뷰어 체크리스트

:laughing: 팀원들은 체크리스트를 보며 코드리뷰를 진행해주세요.

* [ ] **왜 개선이 필요한지 이유를 충분한 설명**해주셨습니까?
* [ ] 작은 커밋 단위만을 보지 말고, **전체 코드의 맥락을 살피면서 리뷰**를 해주세요.
* [ ] **이해가 안가는 부분이 있다면 질문**해주세요.
* [ ] 리뷰를 위한 리뷰를 하지 마세요. **피드백 할 게 없으면 칭찬**해 주세요.:heartbeat:

:raising_hand_tone1:**리뷰를 어떻게 해야할지 모르겠다면 이 글을 한번 봐주세요**

https://tech.kakao.com/2022/03/17/2022-newkrew-onboarding-codereview/

https://blog.logi-spot.com/코드리뷰의-진짜-목적은-따로있다/

https://xo.dev/github-collaboration-guide/ 깃헙에 코드리뷰 요청 보내는법

https://www.youtube.com/watch?v=9FZaYz0s8s4 라매개발자 깃헙 코드리뷰 협업

cf. 체크사항

> ### **배울만한 점은 없는지**
>
> 코드리뷰에 많은 사람이 오해하는 부분 중 하나는, 경력이 많거나 실력이 뛰어난 개발자가 후배 개발자의 코드를 검사한다고 생각하는 것입니다. 코드리뷰에서 코드의 작성자와 리뷰어는 누가 더 경력이 높거나 낮을 필요가 없습니다. 또한, 코드리뷰의 목적은 코드 작성자에 게 피드백을 주는 것도 있지만, 해당 코드를 보면서 ‘아, 이런 식으로 코드를 작성하는 것도 가능하구나?’, ‘아, 이렇게 쉬운 방법이 있었구나?’와 같은 학습효과도 함께 가지고 있습니다. 그렇기 때문에 코드를 리뷰할 때는 피드백을 주기 위한 시각과 좋은 점을 배우려는 시각, 이 두 가지 시각의 균형을 맞추며 진행하는 것이 좋습니다.

우리 프론트엔드팀이 함께 논의하여 정한 팀내 네이밍 규칙

카테고리 없음

읽기 좋은 코드의 작성의 중요성을 알고 코드의 통일감을 위해 팀내에서 정했었던 규칙이다. 참고하셔도 좋을듯 하여 여기 적어봅니다. 

 

네이밍 만으로도 바로 어떤용도의 함수인지 알기 쉽기 위해서 React-Query, api service 호출 hook, interface 등의 네이밍 규칙을 정하였다. 

 

누가 작성하더라도 규칙을 따른다면 누구든 파악하기 쉽고 새로운 멤버 또한 파악하기 쉬워질 것이다. 

따라서 사용하는 단어도 통일하기로 했다. 

 

내부적으로 정한 것이므로 참고하는 용도로 이런식으로도 정하는구나 하고 보면 될것 같다. 

 

---

react-query 작성 규칙

  • query key 는 해당 파일 상단에 작성한다.
  • interface 는 query key 정의해둔 하단부터 시작해 작성한다.
    • RequestDto 먼저 작성하고 ResponseDto를 작성한다. (request를 먼저 보내고 response를 받기 때문에 읽기에 더욱 편함)
    • ex) CharacterRequestDto (Request)
    • ex) CharacterResponseDto (Response)
  • 작성 전 스튜디오 <-> 어드민 간 확인하여 같은 api에 대해서는 일치하도록 작성한다.(스튜디오와 어드민에서 사용하는 api 가 다르므로 파일이 완전히 일치하지는 않음.)
  • 작성 순서 요약
    • query key
    • interface (type 정의)
    • react query hook

Naming 규칙

interface 네이밍 규칙

common.d.ts

  • 공통으로 쓰이는 공통틀은 끝에 Type 으로 네이밍. ex.GetListType
  • 서버에서 결과로 받은 값은 끝에 Dto 를 붙여 네이밍
  • 여러 곳에서 쓰이는 Dto(해당 api 뿐만이 아닌 여러곳에서 사용되는 값) ex. FileImageType 는 /image 외에도 /titles, /chapters 에서도 사용된다

query hooks 네이밍 규칙

네이밍규칙 1: method 관련하여 이렇게 시작

  • get : useGet
  • post: useCreate
  • put: useUpdate
    • create, update 두 가지 역할 할 때에는 ‘usePut’으로 작성
  • delete: useDelete

네이밍규칙 2: id 를 param으로 받아서 한개에 대한 정보를 찾을 경우 네이밍 “useGetOneTitleCategory”