문제설명
스트리밍 사이트에서 장르 별로 가장 많이 재생된 노래를 두 개씩 모아 베스트 앨범을 출시하려 합니다. 노래는 고유 번호로 구분하며, 노래를 수록하는 기준은 다음과 같습니다.
1. 속한 노래가 많이 재생된 장르를 먼저 수록합니다. 2. 장르 내에서 많이 재생된 노래를 먼저 수록합니다. 3. 장르 내에서 재생 횟수가 같은 노래 중에서는 고유 번호가 낮은 노래를 먼저 수록합니다.
노래의 장르를 나타내는 문자열 배열 genres와 노래별 재생 횟수를 나타내는 정수 배열 plays가 주어질 때, 베스트 앨범에 들어갈 노래의 고유 번호를 순서대로 return 하도록 solution 함수를 완성하세요.
제한 사항
- genres[i]는 고유번호가 i인 노래의 장르입니다.
- plays[i]는 고유번호가 i인 노래가 재생된 횟수입니다.
- genres와 plays의 길이는 같으며, 이는 1 이상 10,000 이하입니다.
- 장르 종류는 100개 미만입니다.
- 장르에 속한 곡이 하나라면, 하나의 곡만 선택합니다.
- 모든 장르는 재생된 횟수가 다릅니다.
// 💡 문제 분석 💡
// In : 노래장르(문자열배열), 노래별재생횟수(정수배열) Out : 노래고유번호(인덱스값)
// 노래 고유번호는 genres 와 plays의 index 값
// 많이 재생 장르 > 많이 재생 노래 > 고유번호 낮은 노래
// -> 장르별 플레이 횟수 총 합 비교 / 장르별 재생횟수 기준 내림차순 정렬 필요
// 해시 문제니까, 해시함수 만들기 : 같은 장르는 같은 해시값, 해시값 인덱스에 리스트로 장르별 노래 목록 나열
function solution(genres, plays) {
let answer = []; // 노래고유번호(인덱스값) 순서대로 담은 배열
const genreInfo = []; // 장르별 해시테이블에 자리한 index 값, 총플레이수, 탑2 리스트 저장할 것
const table = new Array(2000);
for (let i = 0; i < genres.length; i++) { // hash table 만들기
setHashTable(genres[i], plays[i], i, table, genreInfo);
}
for (let i = 0; i < genreInfo.length; i++) { // play 횟수별 genreInfo 배열 정리
let first = 0;
let second = 0;
for (let j = 1; j < table[genreInfo[i].idx].length; j++) {
const playTemp = table[genreInfo[i].idx][j].play; // 현재 노래 플레이 횟수
const playNo = table[genreInfo[i].idx][j].no; // 현재 노래 고유번호
genreInfo[i].play += playTemp; // 총 플레이 수를 더해주고
if (playTemp > first) { // 현재 플레이 수가 제일 높은 플레이수보다 크면 교체
genreInfo[i].top[1] = genreInfo[i].top[0]; // 기존에 있던 것 2순위로
genreInfo[i].top[0] = playNo; // top 리스트에 번호넣어주기
second = first;
first = playTemp;
} else if (playTemp === first) { // 수가 같으면 고유번호 비교
if (genreInfo[i].top[0] > playNo) {
genreInfo[i].top[1] = genreInfo[i].top[0]; // 기존에 있던 것 2순위로
genreInfo[i].top[0] = playNo;
} else if (playTemp > second) { // 두번째것과 비교
genreInfo[i].top[1] = playNo;
second = playTemp;
} else if (playTemp === second) {
if (genreInfo[i].top[1] > playNo) {
genreInfo[i].top[1] = playNo;
}
}
} else if (playTemp > second) { // 두번째 것과 비교
genreInfo[i].top[1] = playNo;
second = playTemp;
} else if (playTemp === second) {
if (genreInfo[i].top[1] > playNo) {
genreInfo[i].top[1] = playNo;
}
}
}
}
genreInfo.sort((a, b) => { // 플레이 횟수순으로 장르 정렬
return b.play - a.play;
});
for (let i = 0; i < genreInfo.length; i++) {
for (let j = 0; j < 2; j++) {
if (genreInfo[i].top[j] !== null) {
answer.push(genreInfo[i].top[j]);
}
}
}
return answer;
}
function setHashTable(genre, play, num, table, genreInfo) {
const idx = hashValue(genre);
while (true) {
if (!table[idx]) { // 처음 등록되는 장르
table[idx] = [genre, { play: play, no: num }]; // 0 idx : 장르, 1~ : 노래들 객체로
genreInfo.push({ idx: idx, play: 0, top: [null, null] });
return;
} else if (table[idx][0] === genre) { // 이미 등록된 장르
table[idx].push({ play: play, no: num });
return;
} else { // 충돌의 경우
idx += hashStep(idx);
idx = idx > table.length ? idx - table.length : idx;
}
}
}
function hashValue(s) {
let hash = 17;
for (let i = 0; i < s.length; i++) {
hash = (13 * hash * s.charCodeAt(i)) % 2000;
}
return hash;
}
function hashStep(idx) {
return 17 - (idx % 17);
}
노래 플레이 횟수별로 정렬하는게 너무 길게 작성되었다.
처음에 hash 테이블의 각 인덱스별 배열의 첫번째로 장르이름을 따로 넣으면서 복잡해진 것 같았다..
table[idx] = [genre, { play: play, no: num }]; // 0 idx : 장르, 1~ : 노래들 객체로
그래서 이걸 그냥 genre 까지 객체 안에 넣은 로직으로 코드를 수정해봤다.
// #2
function solution(genres, plays) {
let answer = []; // 노래고유번호(인덱스값) 순서대로 담은 배열
const genreInfo = []; // 장르별 해시테이블에 자리한 index 값, 총플레이수, 탑2 리스트 저장할 것
const table = new Array(2000);
for (let i = 0; i < genres.length; i++) { // hash table 만들기
setHashTable(genres[i], plays[i], i, table, genreInfo);
}
for (let i = 0; i < genreInfo.length; i++) {
table[genreInfo[i].idx].sort((a, b) => { // hash table 안, 각 장르안에서 플레이 횟수순으로 정렬
if (b.play > a.play) {
return 1;
} else if (b.play < a.play) {
return -1;
} else {
return a.no - b.no;
}
})
for (let j = 0; j < table[genreInfo[i].idx].length; j++) {
genreInfo[i].play += table[genreInfo[i].idx][j].play; // 총 플레이 수를 더해주기
}
}
genreInfo.sort((a, b) => { // 플레이 횟수순으로 장르 정렬
return b.play - a.play;
});
for (let i = 0; i < genreInfo.length; i++) {
answer.push(table[genreInfo[i].idx][0].no);
if (table[genreInfo[i].idx][1]) {
answer.push(table[genreInfo[i].idx][1].no);
}
}
return answer;
}
function setHashTable(genre, play, num, table, genreInfo) {
const idx = hashValue(genre);
while (true) {
if (!table[idx]) { // 처음 등록되는 장르
table[idx] = [{ genre: genre, play: play, no: num }];
genreInfo.push({ idx: idx, play: 0 });
return;
} else if (table[idx][0].genre === genre) { // 이미 등록된 장르
table[idx].push({ genre: genre, play: play, no: num });
return;
} else { // 충돌의 경우
idx += hashStep(idx);
idx = idx > table.length ? idx - table.length : idx;
}
}
}
function hashValue(s) {
let hash = 17;
for (let i = 0; i < s.length; i++) {
hash = (13 * hash * s.charCodeAt(i)) % 2000;
}
return hash;
}
function hashStep(idx) {
return 17 - (idx % 17);
}
배열의 각 요소를 같은 구성의 객체로 통일시키니까, 직접 정렬할필요없이 sort로 정렬할 수 있어서 코드가 좀 더 짧아졌다!
시간복잡도도 비슷한데, 조금 더 직관적인 코드로 짠거같아 뿌듯하다ㅎㅎ
객체 배열을 생성할 땐, 구조를 동일하게 하자!!!
해시를 사용하지 않은 다른사람들의 코드들
// #1
function solution(genres, plays) {
var dic = {};
genres.forEach((t,i)=> {
dic[t] = dic[t] ? dic[t] + plays[i] :plays[i];
});
var dupDic = {};
return genres
.map((t,i)=> ({genre : t, count:plays[i] , index:i}))
.sort((a,b)=>{
if(a.genre !== b.genre) return dic[b.genre] - dic[a.genre];
if(a.count !== b.count) return b.count - a.count;
return a.index - b.index;
})
.filter(t=> {
if(dupDic[t.genre] >= 2) return false;
dupDic[t.genre] = dupDic[t.genre] ? dupDic[t.genre]+ 1 : 1;
return true;
})
.map(t=> t.index);
}
코드가 깔끔하고 이해하기 쉽다.
이 문제도 굳이 해시 함수를 거칠 필요없이 객체 리터럴로 데이터를 직접 저장해서 정렬하면 됐다.