Design a class MusicPlayer that supports the following operations:

play(songId: int): Play a song. If the song is already in the playlist, move it to the end (most recently played).
getRecentlyPlayed(n: int): Return the n most recently played unique songs. If there are fewer than n songs in the playlist, return all of them.
getTopSongs(n: int): Return the n most played songs, sorted by play count (descending). If multiple songs have the same play count, sort them by most recently played.

This problem would test your ability to design a system that efficiently manages a playlist, keeps track of play counts, and retrieves recently played and top songs.

Okay let’s see:

 
class MusicPlayer {
	songs: Map<number, number>
	playList: number[]
 
	constructor(){
		songs: Map<number, number> = new Map()
		playlist: number[] = []
	}
	
	playSong(songId: number){
		if (this.songs.has(songId)){
			let s = this.playlist.indexOf(songId)
			this.playlist = [
				...this.playlist.slice(s),
				...this.playlist.slice(s+1, this.playList.length)
			]
			this.songs.set(songId, this.songs.get(songId)+1)
		} else {
			this.songs.set(songId, 1)
		}
		this.playlist.push(songId)
	}
	
	getRecentlyPlayed(n: number): number[] {
		// let's say n is 1
		let pLength = this.playlist.length
		if (n >= pLength) return this.playlist
		return this.playlist.slice(pLength-n, pLength)
		// return n most recently played unique songs
		// if fewer than n songs, return all
	}
	
	getTopSongs(n: number): number[] {
		// Return the n most played songs, sorted by play count (descending).
		// If multiple songs have the same play count, sort them by most recently played.  
	
		for (let i = 0; i < this.playlist.length; i++){
			let plays = this.songs.get(songId)
			this.songs.set({plays: plays, index: i})
		}
		const songCount = this.songs.size
		return [...this.songs.entries()]
			.sort((a, b)=>b[1].plays-a[1].plays)
			.sort((a, b)=>b[1].index-a[1].index)
			.slice(songCount-n, songCount)
		
	}
}

I actually right that code right here in Obsidian. Then I brought it into VScode and cleaned it up to be, eventually, this:

interface hit {
  plays: number
  index: number
}
 
export class MusicPlayer {
	songs: Map<number, number>
	playlist: number[]
 
	constructor(){
		this.songs = new Map()
		this.playlist = []
	}
	
	playSong(songId: number){
    this.playlist = this.playlist.filter(e=>e!==songId)
    this.playlist.push(songId)
    this.songs.set(songId, (this.songs.get(songId) || 0) + 1)
	}
	
	getRecentlyPlayed(n: number): number[] {
		// let's say n is 1
		let pLength = this.playlist.length
		if (n >= pLength) return this.playlist
		return this.playlist.slice(pLength-n, pLength)
		// return n most recently played unique songs
		// if fewer than n songs, return all
	}
	
  getTopSongs(n: number): number[] {
    const hitList: Map<number, hit> = new Map();
 
    for (let i = 0; i < this.playlist.length; i++) {
      let songId = this.playlist[i];
      let plays = this.songs.get(songId) || 0;
      hitList.set(songId, { plays: plays, index: i });
    }
    
    const result = [...hitList.entries()]
      .sort((a, b) => b[1].plays - a[1].plays || b[1].index - a[1].index)
      .map(([id, _]) => id) // Keep only the song IDs
    return result.slice(0, n);
  }
}
 
const player = new MusicPlayer()
player.playSong(1)
player.playSong(2)
player.playSong(1)
 
// .map(({plays, _})=>plays) <- for objects I could do this

But I think I spent two hours on this. I know. It was rough. I was very close, but I got super stuck. I believe that my playSong method was messed up and not adding things correctly.

So algorithmically, things were good almost from the beginning. A couple things to tighten up, but otherwise great.

Codewise, writing everything in one without verifying that the first functions worked was a huge no-no and made it very hard to debug things.

Return #1

I struggled with this one, specifically using data structures in the optimal way. So I looked around to find alternatives. I found this and learned some cool things from it:

class MusicPlayer {
    private songs: Map<number, number> = new Map();
    private playlist: number[] = [];
    private recentlyPlayed: Set<number> = new Set();
 
    playSong(songId: number): void {
        // Update play count
        this.songs.set(songId, (this.songs.get(songId) || 0) + 1);
        
        // Update playlist
        this.playlist = this.playlist.filter(id => id !== songId);
        this.playlist.push(songId);
        
        // Update recently played
        this.recentlyPlayed.delete(songId);
        this.recentlyPlayed.add(songId);
    }
 
    getRecentlyPlayed(n: number): number[] {
        return [...this.recentlyPlayed].slice(-n).reverse();
    }
 
    getTopSongs(n: number): number[] {
        return [...this.songs.entries()]
            .sort((a, b) => {
                if (b[1] !== a[1]) return b[1] - a[1]; // Sort by play count
                return this.playlist.indexOf(b[0]) - this.playlist.indexOf(a[0]); // Then by recency
            })
            .slice(0, n)
            .map(([id]) => id);
    }
}

Key Takeaways

  • maintaining multiple data structures for different use cases isn’t ideal. It just requires keeping track of writes and updates. In this case, the writes and updates are all in one method, so it’s not bad.
  • Sets, in addition to Maps, also keep track of order of insertion
  • Unlike array access, the .slice() method allows for negative indexes! Since the second argument of slice is the end of the slice, exclusive, -1 returns one less at the end of the selection that passing arr.length
  • Using one order set to