[TIL] Flutter 9기 Day 11 알고리즘 문제 & List, Split, File 뜯어보기

현서·2025년 12월 8일

[TIL] Flutter 9기

목록 보기
23/102
post-thumbnail

📍 알고리즘 문제

🕵️‍♂️ Digitville의 장난꾸러기 두 숫자

문제 링크

* 문제
Digitville 마을에는 0부터 n-1까지의 숫자가 각각 한 번씩만 등장해야 하는 리스트 nums가 있었다.
하지만 두 개의 장난꾸러기 숫자가 한 번 더 숨어 들어가 리스트가 원래 길이보다 길어져 버렸다.
당신은 이 마을의 탐정으로서, 두 번 등장한 숫자 두 개를 찾아내야 한다.
순서는 상관 없으며, 두 숫자를 배열 형태로 반환하면 된다.

* 예시 1
입력: nums = [0,1,1,0]
출력: [0,1]
설명: 01이 각각 두 번 등장했다.

* 예시 2
입력: nums = [0,3,2,1,3,2]
출력: [2,3]
설명: 23이 두 번씩 등장했다.

* 예시 3
입력: nums = [7,1,5,4,3,4,6,0,9,5,8,2]
출력: [4,5]
설명: 45가 두 번 등장했다.

* 조건
2 <= n <= 100
nums.length == n + 2
0 <= nums[i] < n

리스트에는 정확히 두 숫자만 중복된다.

일단 2숫자만 중복이 되므로 리스트에서 중복되는 숫자 2개만 찾으면 코드가 끝나게 만들면 될 것 같다.

class Solution {
  List<int> getSneakyNumbers(List<int> nums) {
    nums.sort();
    List<int> sneakyNum = [];

    for (var i = 0; i < nums.length - 1; i++) {
      if (nums[i] == nums[i + 1]) {
        sneakyNum.add(nums[i]);
      }
    }

    if (sneakyNum.length != 2) {
      sneakyNum.add(nums[nums.length - 1]);
    }

    return sneakyNum;
  }
}

숫자들을 오름차순으로 정렬해서 현재 nums[i]과 바로 뒤에 있는 nums[i]가 같다면 sneakyNum에 추가해서 sneakyNum을 return하게 만들었는데 런타임이 6ms가 걸렸다. 다른 사람 통계에 1ms도 있길래 시간을 줄일 방법이 있나 생각해봤다.

새로 map으로 저장해서 똑같은 key가 있다면 value 값음 +1 하고 value 값이 2인 key만 내보내는 방법을 생각했다.

class Solution {
  List<int> getSneakyNumbers(List<int> nums) {
    Map<int, int> sneakyNum = {};
    List<int> sneakyNums = [];

    for (var i = 0; i < nums.length; i++) {
      sneakyNum.update(nums[i], (value) => (value) + 1, ifAbsent: () => 1);
    }

    for (var n in sneakyNum.entries) {
      if (n.value == 2) {
        sneakyNums.add(n.key);
      }
    }

    return sneakyNums;
  }
}

update를 사용해서 만들었는데 이게 런타임이 더 걸린다는 사실~ 그만 생각하기로 했다..

💎 보석과 돌

* 문제
문자열 jewels는 보석의 종류를 나타내고, 문자열 stones는 당신이 가진 돌들을 나타낸다.
stones의 각 문자(문자 하나)는 당신이 가진 돌의 종류이다. 당신이 가진 돌들 중에서 보석에 해당하는 것이 몇 개인지 알고 싶다.

문자는 대소문자를 구분하므로 "a""A"와 다른 종류로 취급된다.

* 예시 1:
입력: jewels = "aA", stones = "aAAbbbb"
출력: 3

* 예시 2: 입력: jewels = "z", stones = "ZZ"
출력: 0

* 제한 조건:
1 <= jewels.length, stones.length <= 50
jewels와 stones는 영어 알파벳만으로 이루어져 있다.
jewels의 모든 문자는 중복되지 않고 유일하다.

리스트로 되어있을 때는 뭔가 비교하기 쉬울 것 같은데 문자열로 되어있어서 비교하기 어려웠다.. 그래서 찾아보니 split()을 사용하면 문자열을 나눠서 List에 넣을 수 있다는 걸 보고 split에 대해 공부한 후 다시 풀었다.

class Solution {
  int numJewelsInStones(String jewels, String stones) {
    int count = 0;
    List<String> jewelsList = jewels.split('');
    List<String> stonesList = stones.split('');

    for (var i = 0; i < jewelsList.length; i++) {
      if (stonesList.contains(jewelsList[i])) {
        count++;
      }
    }

    return count;
  }
}

처음에 이렇게 코드를 짰는데 생각해보니 이 방법은 jewelsList.length 길이만 반복해서 다른 반목문을 사용하는 게 맞다.. ㅋㅁㅋ..

class Solution {
  int numJewelsInStones(String jewels, String stones) {
    int count = 0;
    List<String> jewelsList = jewels.split('');
    List<String> stonesList = stones.split('');

    for (var s in stonesList) {
      if (jewelsList.contains(s)) {
        count++;
      }
    }

    return count;
  }
}

stonesList가 더 값이 기니 stonesList를 돌면서 jewelsList에 값이 있는지 확인하는 게 더 나은 방법이담 이렇게 제출하니 성공~

📍 튜터님과 위젯 공부

Floating Action Button

공식 문서

FAB란?

화면 위에 떠 있는 주요 액션 버튼
앱에서 가장 핵심 동작에 사용
보통 화면 한 곳에 고정되며 스크롤해도 그대로 남음

기본 구조

Scaffold(
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    child: const Icon(Icons.add),
  ),
);

Scaffold 안에 위치

확장형(텍스트 포함)

FloatingActionButton.extended(
  onPressed: () {},
  icon: const Icon(Icons.edit),
  label: const Text('작성하기'),
);

위치 변경

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
옵션위치
endFloat오른쪽 아래 기본 위치
centerFloat아래 중앙
startFloat왼쪽 아래
centerDockedBottomBar 중앙에 파묻힌 디자인
endDockedBottomBar 오른쪽 결합

스타일 변경

FloatingActionButton(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  backgroundColor: Colors.red,
  elevation: 4,
  child: const Icon(Icons.add, size: 28),
)

FAB VS BottomBar 아이콘

요소목적
Bottom Navigation Icon화면 이동
Floating Action Button핵심 동작 수행

List 뜯어 보기

✏️ List란?

길이를 가지고 있고 인덱스로 접근 가능한 컬렉션
이 클래스를 상속한 리스트들은 두 종류로 나뉜다:

  • 고정 길이 리스트 (Fixed-length list)
  • 가변 길이 리스트 (Growable list)

고정 길이 리스트 (Fixed-length list)

길이를 변경할 수 없음
요소 변경은 가능하지만 추가/삭제 불가

final fixedLengthList = List<int>.filled(5, 0);
fixedLengthList[0] = 87; // 가능
fixedLengthList.length = 0;  // 오류
fixedLengthList.add(499);    // 오류

가변 길이 리스트 (Growable list)

길이 변경 가능
add, addAll, insert, remove 등 전체 API 사용 가능

final growableList = <String>['A', 'B'];
growableList.add('X');
growableList.remove('A');
growableList.insert(1, 'New');

✏️ 주요 메서드

기능메서드
값 추가add, addAll, insert, insertAll
값 변경[]=, replaceRange, fillRange, setRange
값 삭제remove, removeAt, removeLast, removeRange, removeWhere
값 위치 확인indexOf, lastIndexOf
정렬sort
섞기shuffle
조건에 맞는 첫 요소 찾기firstWhere
마지막 요소 찾기lastWhere, singleWhere

추가 / 삽입

  • add(element) — 끝에 추가
  • addAll(iterable) — 여러 요소 추가
  • insert(index, element) — 특정 위치에 삽입
    insertAll(index, iterable) — 여러 개 삽입
final lst = [1,2];
lst.add(3);           // [1,2,3]
lst.addAll([4,5]);    // [1,2,3,4,5]
lst.insert(1, 9);     // [1,9,2,3,4,5]

고정 리스트는 이메서드 사용 불가 → UnsupportedError

삭제

  • remove(value) — 첫 번째 일치 항목 삭제, 성공 시 true
  • removeAt(index) — 인덱스 삭제, 삭제된 값 반환
  • removeLast() — 마지막 요소 삭제, 반환
  • removeRange(start, end) — 범위 삭제
  • removeWhere((e) => condition) — 조건에 맞는 모든 요소 삭제
  • clear() — 전체 삭제
final lst = [1,2,3,2];
lst.remove(2);       // [1,3,2]
var x = lst.removeAt(1); // x=3, [1,2]
lst.removeLast();    // [1]

반복 중에 remove로 길이 변경하면 ConcurrentModificationError 발생 가능

검색 / 포함

  • indexOf(value, [start=0]) — 첫 인덱스 또는 -1
  • lastIndexOf(value, [start]) — 마지막 인덱스
  • contains(value) — 포함 여부
  • elementAt(index) — index 접근과 동일
[1,2,3,2].indexOf(2); // 1
[1,2,3].contains(4); // false

범위 연산 (복사·채우기·교체)

  • sublist(start, [end]) — 부분 리스트 복사(새 리스트 반환)
  • setRange(start, end, iterable, [skipCount=0]) — 주어진 iterable 값을 지정 범위에 복사 (end는 포함 x)
  • replaceRange(start, end, iterable) — 범위 삭제 후 삽입
  • fillRange(start, end, fill) — 범위 값을 동일 값으로 채움
  • setAll(index, iterable) — index부터 iterable로 덮어쓰기
var a = [0,1,2,3,4];
var s = a.sublist(1,4);         // [1,2,3]
a.setRange(1,3, [9,9]);         // [0,9,9,3,4]
a.fillRange(0,2, 5);            // [5,5,9,3,4]
a.replaceRange(2,4, [7,8,9]);   // [5,5,7,8,9,4]

setRange는 복사(source와 destination이 겹칠 때 내부적으로 안전 처리하지만 구현에 따라 주의)

정렬/섞기

  • sort([compare]) — 제자리 정렬(in-place) 기본은 자연 순서
var l = [3,1,2];
l.sort(); // [1,2,3]
// 커스텀 비교:
l.sort((a,b) => b.compareTo(a)); // 내림차순
  • shuffle([Random rnd]) — 무작위 섞기

sort 도중 리스트를 수정하면 ConcurrentModificationError 발생 가능
비교 함수는 일관된 비교(반사성·대칭성·추이성)을 지켜야 함

📍 Split

✏️ Split

split()은 기준을 만나면 그 지점에서 잘라서 리스트로 돌려줌

'Hello world'.split(' ');

공백을 기준으로 자름 → 결과: ['Hello', 'world']

✏️ 기준이 문자열일 때

'a,b,c'.split(',');

, 기준으로 자름
결과: ['a', 'b', 'c']

'1,,2'.split(',');

,가 연속으로 나오면 그 사이에 내용이 없으니까 빈 문자열 포함
결과: ['1', '', '2']

✏️ 기준이 정규식(RegExp)일 때

'a  b   c'.split(RegExp(r'\s+'));

연속 공백 전부를 기준으로 한 번에 자름
결과: ['a', 'b', 'c']

✏️ 기준이 하나도 안 맞을 때

'hello'.split('x');
x가 없음 → 안 잘림
결과: ['hello'] (그냥 원본 하나)

✏️ 빈 문자열로 자를 때 ('')

'Pub'.split('');
글자 하나씩 다 쪼갬
결과: ['P', 'u', 'b']

유니코드 기준이 아니라 UTF-16 코드 단위로 쪼갬
그래서 이모지 같은 건 반쪽으로 쪼개질 수 있음

✏️ 빈 문자열 자체를 split할 때

''.split('');

결과: []
비어있어서 쪼갤 게 없음

''.split('a');

결과: []
매치도 없고 쪼갤 것도 없음

📍 File

File class란?

파일 시스템의 파일 경로를 참조하는 객체
파일 읽기/쓰기/삭제/확인 등을 수행
부모 디렉터리는 parent로 접근

var myFile = File('file.txt');

읽는 방법

파일 전체 한 번에 읽기 (작은 파일)

var text = await File('file.txt').readAsString();
print(text);

조금씩 나눠 읽기 (큰 파일)

var lines = File('file.txt').openRead()
  .transform(utf8.decoder)
  .transform(LineSplitter());

await for (var line in lines) {
  print(line);
}

쓰는 방법

한 번에 쓰기

await File('file.txt').writeAsString('내용');

스트림 방식 (이어 쓰기 가능)

var sink = File('file.txt').openWrite();
sink.write('로그 추가\n');
await sink.flush();
await sink.close(); // 반드시 닫기

공부 소감

오늘부터 개인과제를 시작했는데 아직 객체와 클래스 부분이 헷갈린다.. 뭔가 익숙한 느낌인 것 같으면서 알쏭달쏭하달까.. 다시 내가 쓴 벨로그를 보면서 복습과 동시에 과제를 하고 있다. 이 부분은 앞으로 자주 쓰일 것 같아서 익히고 넘어가는 게 좋을 것 같담

0개의 댓글