[프로그래머스 lev3/JS] 매칭점수

woolee의 기록보관소·2023년 3월 28일
0

알고리즘 문제풀이

목록 보기
172/178

문제 출처

프로그래머스 lev3 - 매칭점수

나의 풀이 (실패)

정규표현식은 어렵다.

다른 풀이

풀이 1

반드시 정규표현식만 고집할 필요는 없다.
모든 걸 전부 정규표현식으로 구현하면 어렵지만,
includes 메서드를 적절히 활용하면 정규표현식 자체는 어렵게 구성하지 않아도 된다.

  • REGEX_WORD는 숫자와 특수기호들을 찾는 정규식이다. \d는 숫자를 찾고 \W는 특수기호(알파벳, 숫자,언더바(_)가 아닌 문자들)를 찾는다. REGEX_WORD는 나중에 split의 매개변수로 넣어서 숫자와 특수기호를 제외한 문자들을 찾을 때 사용한다.
  • REGEX_URL은 외부 링크를 찾을 때 사용될 정규식이다. \S는 공백이 아닌 하나의 문자와 매칭된다. *를 붙여줘서 연속적으로 찾을 수 있게 해준다. REGEX_URL은 match 메서드의 매개변수로 넣을 정규식이다.
  • META_URL은 메타 태그에 존재하는 내 url을 찾을 때 사용된다.

pageInfo에는 { 자기 url, 기본점수, 외부 링크, 각각의 인덱스, 매칭 점수 }가 담긴다.

page를 \n으로 분리한다. \n을 정규식에서 쓰면 얘는 이 자체로 인식된다. 즉, \\n 뭐 이런 식으로 안 써도 된다. (pageArr)

그리고 META_URL이 있는 위치를 찾아서 자기 url를 찾는다(urlIdx => pageURL)

body 태그가 시작하는 부분과 끝나는 부분을 찾아서, body 내용을 추출한 다음에, .flatMap((str) => str.toLowerCase().split(REGEX_WORD))를 사용해서 특수 기호를 제외한 나머지 중에서, .filter((e) => e === word).length;를 사용해서 일치하는 단어의 개수를 측정해준다.

REGEX_URL을 사용해서 외부 링크들도 구해준다.
이렇게 해서 각 pageURL가 가지고 있는 pageInfo를 구해준다.

pageInfo를 반복하면서 점수를 계산해주고 이를 바탕으로 matchPoint를 구해준다.

만약 겹치는 경우 우선하는 인덱스를 답으로 찾아야 하기 때문에 sort를 해준다.

function solution(word, pages) {
  word = word.toLowerCase();
  const REGEX_WORD = /[\d|\W]/;
  const REGEX_URL = /<a href="https:\S*"/gi;
  const META_URL = "meta property";
  const pageInfo = new Map();

  pages.forEach((page, idx) => {
    const pageArr = page.split("\n");
    const urlIdx = pageArr.findIndex((el) => el.includes(META_URL));
    const pageURL = pageArr[urlIdx].match(/"https:\S*"/gi)[0];
    const bodyStart = pageArr.findIndex((el) => el.includes("<body>"));
    const bodyEnd = pageArr.findIndex((el) => el.includes("</body>"));
    const body = pageArr.slice(bodyStart + 1, bodyEnd);
    const point = body
      .flatMap((str) => str.toLowerCase().split(REGEX_WORD))
      .filter((e) => e === word).length;
    const outLinks = body
      .flatMap((str) => str.match(REGEX_URL))
      .filter((e) => e) // null 제거하기 위해
      .map((e) => e.substr(8, e.length)); // url만 남기기 위해

    pageInfo.set(pageURL, { point, outLinks, idx, matchPoint: 0 });
  });

  for (const [key, value] of pageInfo) {
    const linkPoint = value.point / value.outLinks.length;

    for (const link of value.outLinks) {
      if (pageInfo.has(link)) {
        const origin = pageInfo.get(link);
        const calculatedPoint = origin.matchPoint
          ? origin.matchPoint + linkPoint
          : origin.point + linkPoint;
        pageInfo.set(link, { ...origin, matchPoint: calculatedPoint });
      }
    }
  }

  const answer = [];

  for (const [key, value] of pageInfo) {
    const { point, idx, matchPoint } = value;
    const finalPoint = matchPoint ? matchPoint : point;
    answer.push([idx, finalPoint]);
  }

  answer.sort((a, b) => (a[1] === b[1] ? a[0] - b[0] : b[1] - a[1]));

  return answer[0][0];
}

찾아야 할 데이터가 많을 때 map을 활용하면 좋다.
하나의 데이터에 달려 있는 수많은 데이터들을 객체로 묶어 매칭되도록 하기 좋은 구조를 가지고 있기 때문이다.

Array.prototype.flatMap()
각 요소에 대해 map을 진행하고 결과를 새로운 배열로 평탄화해준다.

let arr1 = ["it's Sunny in", "", "California"];

arr1.map(x=>x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]

arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in","","California"]

flatMap은 map을 하는 과정에서 아이템을 추가하거나 제거하여 아이템 개수를 바꿀 수 있다. 즉, 각각의 아이템에 대하여 1:1대응관계 뿐만 아니라 다대다 대응도 가능하다는 것다. 이러한 측면에서 filter가 하는 역할의 반대역할을 한다고 볼 수 있다. 단순히 아무런 변화를 주고 싶지 않을때에는 원소 하나를 가진 배열을 반환할 수도, 아이템을 추가하고 싶을 때는 다-원소 배열을 반환할 수도, 아이템을 제거하고 싶을 때에는 빈 배열을 반환할 수도 있다.

// 다음은 음수는 제거하고 홀수는 1과 짝수로 분리하는 예시입니다.
    let a = [5, 4, -3, 20, 17, -33, -4, 18]
    //       |\  \  x   |  | \   x   x   |
    //      [4,1, 4,   20, 16, 1,       18]

    a.flatMap( (n) =>
      (n < 0) ?      [] :
      (n % 2 == 0) ? [n] :
                     [n-1, 1]
    )

    // expected output: [4, 1, 4, 20, 16, 1, 18]
    //=> [1, 2, 3, 4, 5, 6, 7, 8, 9]

flatMap은 reduce와 concat을 조합해서 구현할 수 있다.

var arr1 = [1, 2, 3, 4];

arr1.flatMap(x => [x * 2]);
// 다음과 동일합니다
arr1.reduce((acc, x) => acc.concat([x * 2]), []);
// [2, 4, 6, 8]
//=> [1, 2, 3, 4, 5, 6, 7, 8, 9]

풀이 2

대괄호 안에서의 ^는 not을 표현한다.

function solution(word, rawPages) {
  word = word.toLowerCase();
  return rawPages
    .map((rawPage, index) => {
      const [_, url] = rawPage.match(
        /<meta property="og:url" content="([^"]+)"\/>/i
      );
      const tags = rawPage.match(/<[^>]+>/g);
      const basicScore = tags
        .reduce((raw, tag) => raw.replace(tag, ""), rawPage)
        .split(/[^a-zA-Z]/)
        .filter((w) => w.toLowerCase() == word).length;
      const outUrls = tags.reduce((links, tag) => {
        const result = tag.match(/<a href="([^"]+)">/i);
        return result ? links.concat(result[1]) : links;
      }, []);

      return {
        index,
        url,
        outUrls,
        basicScore,
      };
    })
    .map((page, index, pages) => {
      page.linkScore = pages
        .filter((p) => p.outUrls.some((url) => url == page.url))
        .reduce((sum, p) => sum + p.basicScore / p.outUrls.length, 0);
      page.totalScore = page.linkScore + page.basicScore;
      return page;
    })
    .sort((a, b) =>
      a.totalScore == b.totalScore
        ? a.index - b.index
        : b.totalScore - a.totalScore
    )[0].index;
}

정규표현식 정리

표현식

^x : 문자열의 시작을 표현하며 x 문자로 시작됨을 의미한다.
x$ : 문자열의 종료를 표현하며 x 문자로 종료됨을 의미한다.
.x : 임의의 한 문자의 자리수를 표현하며 문자열이 x 로 끝난다는 것을 의미한다.
x+ : 반복을 표현하며 x 문자가 한번 이상 반복됨을 의미한다.
x? : 존재여부를 표현하며 x 문자가 존재할 수도, 존재하지 않을 수도 있음을 의미한다.
x* : 반복여부를 표현하며 x 문자가 0번 또는 그 이상 반복됨을 의미한다.
x|y : or 를 표현하며 x 또는 y 문자가 존재함을 의미한다.
(x) : 그룹을 표현하며 x 를 그룹으로 처리함을 의미한다.
(x)(y) : 그룹들의 집합을 표현하며 앞에서 부터 순서대로 번호를 부여하여 관리하고 x, y 는 각 그룹의 데이터로 관리된다.
(x)(?:y) : 그룹들의 집합에 대한 예외를 표현하며 그룹 집합으로 관리되지 않음을 의미한다.
x{n} : 반복을 표현하며 x 문자가 n번 반복됨을 의미한다.
x{n,} : 반복을 표현하며 x 문자가 n번 이상 반복됨을 의미한다.
x{n,m} : 반복을 표현하며 x 문자가 최소 n번 이상 최대 m 번 이하로 반복됨을 의미한다.

[xy] : 문자 선택을 표현하며 x 와 y 중에 하나를 의미한다.
[^xy] : not 을 표현하며 x 및 y 를 제외한 문자를 의미한다.
[x-z] : range를 표현하며 x ~ z 사이의 문자를 의미한다.
\^ : escape 를 표현하며 ^ 를 문자로 사용함을 의미한다.
\b : word boundary를 표현하며 문자와 공백사이의 문자를 의미한다.
\B : non word boundary를 표현하며 문자와 공백사이가 아닌 문자를 의미한다.
\d : digit 를 표현하며 숫자를 의미한다.
\D : non digit 를 표현하며 숫자가 아닌 것을 의미한다.
\s : space 를 표현하며 공백 문자를 의미한다.
\S : non space를 표현하며 공백 문자가 아닌 것을 의미한다.
\t : tab 을 표현하며 탭 문자를 의미한다.
\v : vertical tab을 표현하며 수직 탭(?) 문자를 의미한다.
\w : word 를 표현하며 알파벳 + 숫자 + _ 중의 한 문자임을 의미한다.
\W : non word를 표현하며 알파벳 + 숫자 + _ 가 아닌 문자를 의미한다.

Flag
g : Global 의 표현하며 대상 문자열내에 모든 패턴들을 검색하는 것을 의미한다.
i : Ignore case 를 표현하며 대상 문자열에 대해서 대/소문자를 식별하지 않는 것을 의미한다.
m : Multi line을 표현하며 대상 문자열이 다중 라인의 문자열인 경우에도 검색하는 것을 의미한다.

검색 기준

| : or
[] : 괄호 안 문자 중 하나
[^문자] : 괄호 안 문자 제외
^문자열 : 특정 문자열로 시작
문자열$ : 특정 문자열로 끝남

반복

? : 없거나 최대 1개
* : 없거나 있거나 (여러 개)
+ : 최소 1개 또는 여러 개
*? : 없거나 최대 1개 ({0}과 동일)
+? : 최소 1개 ({1}과 동일)
{n} : n개
{min, } : 최소 min 개 이상
{min, max} : 최소 min 개 이상, 최대 max 개 이하

패턴 그룹

() : 그룹화 및 캡쳐
(?: 패턴) : 그룹화 (캡쳐 X)
(?=) : 앞쪽 일치 /ab(?=c)/
(?!) : 부정 앞쪽 일치 /ab(?!c)/
(?<=) : 뒤쪽 일치 /(?<=ab)c/
(?<!) : 부정 뒤쪽 일치 /(?<!ab)c/

생성

// 리터럴 방식
const regex = /abc/;

// 생성자 방식
const regex = new RegExp("abc");
const regex = new RegExp(/abc/); 

메서드

("문자열").match(/정규표현식/플래그) : "문자열"에서 "정규표현식"에 매칭되는 항목들을 배열로 반환
("문자열").replace(/정규표현식/, "대체문자열") : "정규표현식"에 매칭되는 항목을 "대체문자열"로 변환
("문자열").split(정규표현식) : "문자열"을 "정규표현식"에 매칭되는 항목으로 쪼개어 배열로 반환 (해당 정규표현식을 제외하고 찾고 싶을 때)
(정규표현식).test("문자열") : "문자열"이 "정규표현식"과 매칭되면 true, 아니면 false반환
(정규표현식).exec("문자열") : match메서드와 유사(단, 무조건 첫번째 매칭 결과만 반환)

혹은
("문자열").includes(/정규표현식/플래그)
("문자열").includes("문자열")

참고

[프로그래머스] LV.3 매칭점수 (JS)
정규표현식 (Regex) 정리
[JS] 📚 정규표현식(RegExp) - 이해하기 쉽게 정리 + 응용 예제

profile
https://medium.com/@wooleejaan

0개의 댓글