Woodstock Blog

a tech blog for general algorithmic interview questions

[CC150v5] 8.9 Design a In-memory File System

Question

Explain the data structures and algorithms that you would use to design an in-memory file system. Illustrate with an example in code where possible.

Solution

A file system consists of Files and Directories. Each Directory contains a set of Files and Directories.

Since Files and Directories share so many characteristics, we’ve implemented them such that they inherit from the same class – Entry.

Code

Entry.java

public abstract class Entry {
    protected Directory parent;
    protected long created;
    protected long lastUpdated;
    protected long lastAccessed;
    protected String name;

    public Entry(String n, Directory p) {
        name = n;
        parent = p;
        created = System.currentTimeMillis();
    }

    public boolean delete() {
        if (parent == null) {
            return false;
        }
        return parent.deleteEntry(this);
    }

    public abstract int size();

    public String getFullPath() {
        if (parent == null) {
            return name;
        } else {
            return parent.getFullPath() + "/" + name;
        }
    }

    public long getCreationTime() {
        return created;
    }

    public long getLastUpdatedTime() {
        return lastUpdated;
    }

    public long getLastAccessedTime() {
        return lastAccessed;
    }

    public void changeName(String n) {
        name = n;
    }

    public String getName() {
        return name;
    }
}

Directory.java

public class Directory extends Entry {
    protected ArrayList<Entry> contents;

    public Directory(String n, Directory p) {
        super(n, p);
        contents = new ArrayList<Entry>();
    }

    protected ArrayList<Entry> getContents() {
        return contents;
    }

    public int size() {
        int size = 0;
        for (Entry e : contents) {
            size += e.size();
        }
        return size;
    }

    public int numberOfFiles() {
        int count = 0;
        for (Entry e : contents) {
            if (e instanceof Directory) {
                count++; // Directory counts as a file
                Directory d = (Directory) e;
                count += d.numberOfFiles();
            } else if (e instanceof File) {
                count++;
            }
        }
        return count;
    }

    public boolean deleteEntry(Entry entry) {
        return contents.remove(entry);
    }

    public void addEntry(Entry entry) {
        contents.add(entry);
    }
}

File.java

public class File extends Entry {
    private String content;
    private int size;

    public File(String n, Directory p, int sz) {
        super(n, p);
        size = sz;
    }

    public int size() {
        return size;
    }

    public String getContents() {
        return content;
    }

    public void setContents(String c) {
        content = c;
    }
}

[CC150v5] 8.10 Implement a Hashmap

Question

Design and implement a hash table which uses chaining (linked lists) to handle collisions.

Solution

I wrote this topic before (using Java source code as a reference). This time, I would like to take another (easier) approach.

The main part of this is to use an array of list structure like this:

LinkedList<Cell<K, V>>[] items;

And the Cell Class looks like this:

public class Cell<K, V> {
    private K key;
    private V value;

    public Cell(K k, V v) {
        key = k;
        value = v;
    }

    public boolean equivalent(Cell<K, V> c) {
        return equivalent(c.getKey());
    }

    public boolean equivalent(K k) {
        return key.equals(k);
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

Code

written by me

public class HashMapCC150<K, V> {

    int size;
    LinkedList<Cell<K, V>>[] items;

    public HashMapCC150(int num) {
        this.size = num;
        items = (LinkedList<Cell<K, V>>[]) new LinkedList[10];
    }

    public V get(K k) {
        int hashValue = this.calculateHashCode(k);
        if (items[hashValue] == null) {
            items[hashValue] = new LinkedList<Cell<K, V>>();
            return null;
        }
        for (Cell<K, V> cell : items[hashValue]) {
            if (k.equals(cell.getKey())) {
                return cell.getValue();
            }
        }
        return null;
    }

    public V put(K k, V v) {
        int hashValue = this.calculateHashCode(k);
        if (items[hashValue] == null) {
            items[hashValue] = new LinkedList<Cell<K, V>>();
        }
        for (Cell<K, V> cell : items[hashValue]) {
            if (k.equals(cell.getKey())) {
                items[hashValue].remove(cell);
                break;
            }
        }
        Cell<K, V> newItem = new Cell<K, V>(k, v);
        items[hashValue].add(newItem);
        return null;
    }

    public V remove(K k) {
        // not written
        return null;
    }

    private int calculateHashCode(K k) {
        return k.toString().length() % size;
    }

    public static void main(String[] args) {
        HashMapCC150<String, String> map = new HashMapCC150<String, String>(10);
        map.put("kevin", "durant");
        map.put("steven", "curry");
        map.put("al", "jefferson");
        System.out.println(map.get("kevin"));
        System.out.println(map.get("steven"));
        System.out.println(map.get("al"));
        map.put("kevin", "martin");
        map.put("steven", "nash");
        map.put("kevin", "braynt");
        System.out.println(map.get("kevin"));
        System.out.println(map.get("steven"));
        System.out.println(map.get("al"));
    }
}

class Cell<K, V> {
    private K key;
    private V value;

    public Cell(K k, V v) {
        key = k;
        value = v;
    }

    public boolean equivalent(Cell<K, V> c) {
        return equivalent(c.getKey());
    }

    public boolean equivalent(K k) {
        return key.equals(k);
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }
}

[CC150v5] 8.8 Design Othello Game

Question

Othello is played as follows: Each Othello piece is white on one side and black on the other. When a piece is surrounded by its opponents on both the left and right sides, or both the top and bottom, it is said to be captured and its color is flipped.

The win is assigned to the person with the most pieces. Implement the object-oriented design for Othello.

Class

  1. Game
    1. Two Player objects
    2. a board object
    3. singleton class (unless otherwise specified, this needs to be discussed)
  2. Board
    1. Keep the score (black/white count). Of course we can put score in Game Class as well, it seems logically related to the board a bit more.
    2. Array of Piece objects
    3. getScore() method
  3. Piece
    1. stores color info
    2. flip() function
  4. Player
    1. stores color info
    2. playPiece() method
  5. Color Enum
  6. Direction Enum

Functions

  1. placePiece() by the Player (which triggers the following 2 methods)
  2. private flipSection(Position fromWhere, Color c, Direction up/down/left/right) by the board
  3. private updateScore() by the board
  4. getScore() by the board

Follow up: Do we need separate Board and Game classes?

Strictly speaking, no. The drawback is adding an extra layers. A function that calls Game class is immediately calling Board class.

But keeping the objects separate allows us to have a logical separation between the board (which contains just logic involving placing pieces) and the game (which involves times, game flow, etc.).

Conclusion

This is an easy, and very standard OOD question. Keep the logic clear, design the layers and object hierarchy, and the rest of things will come naturally.

Code

Game.java

public class Game {
    private Player[] players;
    private static Game instance;
    private Board board;
    private final int ROWS = 10;
    private final int COLUMNS = 10;

    private Game() {
        board = new Board(ROWS, COLUMNS);
        players = new Player[2];
        players[0] = new Player(Color.Black);
        players[1] = new Player(Color.White);
        Automator.getInstance().initialize(players); // used for testing
    }

    public static Game getInstance() {
        if (instance == null) {
            instance = new Game();
        }
        return instance;
    }

    public Board getBoard() {
        return board;
    }
}

Board.java

public class Board {
    private int blackCount = 0;
    private int whiteCount = 0;
    private Piece[][] board;

    public Board(int rows, int columns) {
        board = new Piece[rows][columns];
    }

    public void initialize() {
        /* initial board has a grid like the following in the center:
         *     WB
         *     BW
         */
        int middleRow = board.length / 2;
        int middleColumn = board[middleRow].length / 2;
        board[middleRow][middleColumn] = new Piece(Color.White);
        board[middleRow + 1][middleColumn] = new Piece(Color.Black);
        board[middleRow + 1][middleColumn + 1] = new Piece(Color.White);
        board[middleRow][middleColumn + 1] = new Piece(Color.Black);
        blackCount = 2;
        whiteCount = 2;
    }

    public boolean placeColor(int row, int column, Color color) {
        if (board[row][column] != null) {
            return false;
        }

        /* attempt to flip each of the four directions */
        int[] results = new int[4];
        results[0] = flipSection(row - 1, column, color, Direction.up);
        results[1] = flipSection(row + 1, column, color, Direction.down);
        results[2] = flipSection(row, column + 1, color, Direction.right);
        results[3] = flipSection(row, column - 1, color, Direction.left);

        /* compute how many pieces were flipped */
        int flipped = 0;
        for (int result : results) {
            if (result > 0) {
                flipped += result;
            }
        }

        /* if nothing was flipped, then it's an invalid move */
        if (flipped < 0) {
            return false;
        }

        /* flip the piece, and update the score */
        board[row][column] = new Piece(color);
        updateScore(color, flipped + 1);
        return true;
    }

    private int flipSection(int row, int column, Color color, Direction d) {
        /* Compute the delta for the row and the column. At all times, only the row or the column
         * will have a delta, since we're only moving in one direction at a time.
         */
        int r = 0;
        int c = 0;
        switch (d) {
        case up:
            r = -1;
            break;
        case down:
            r = 1;
            break;
        case left:
            c = -1;
            break;
        case right:
            c = 1;
            break;
        }

        /* If out of bounds, or nothing to flip, return an error (-1) */
        if (row < 0 || row >= board.length || column < 0 || column >= board[row].length || board[row][column] == null) {
            return -1;
        }

        /* Found same color - return nothing flipped */
        if (board[row][column].getColor() == color) {
            return 0;
        }

        /* Recursively flip the remainder of the row. If -1 is returned, then we know we hit the boundary
         * of the row (or a null piece) before we found our own color, so there's nothing to flip. Return
         * the error code.
         */
        int flipped = flipSection(row + r, column + c, color, d);
        if (flipped < 0) {
            return -1;
        }

        /* flip our own color */
        board[row][column].flip();
        return flipped + 1;
    }

    public int getScoreForColor(Color c) {
        if (c == Color.Black) {
            return blackCount;
        } else {
            return whiteCount;
        }
    }

    private void updateScore(Color newColor, int newPieces) {
        /* If we added x pieces of a color, then we actually removed x - 1 pieces of the other
         * color. The -1 is because one of the new pieces was the just-placed one.
         */
        if (newColor == Color.Black) {
            whiteCount -= newPieces - 1;
            blackCount += newPieces;
        } else {
            blackCount -= newPieces - 1;            
            whiteCount += newPieces;
        }
    }

    public void printBoard() {
        for (int r = 0; r < board.length; r++) {
            for (int c = 0; c < board[r].length; c++) {
                if (board[r][c] == null) {
                    System.out.print("_");
                } else if (board[r][c].getColor() == Color.White) {
                    System.out.print("W");
                } else {
                    System.out.print("B");
                }
            }
            System.out.println();
        }
    }
}

Player.java

public class Player {
    private Color color;
    public Player(Color c) {
        color = c;
    }

    public int getScore() {
        return Game.getInstance().getBoard().getScoreForColor(color);
    }

    public boolean playPiece(int row, int column) {
        return Game.getInstance().getBoard().placeColor(row, column, color);
    }

    public Color getColor() {
        return color;
    }
}

[CC150v5] 8.7 Design Online Chat Server (2)

… Continued from previous post.

Overall view

The system consists of a database, a set of clients, and a set of servers. This is not about OOD, but we need to know.

  1. DB stores user list, chat archive. An SQL DB would be good, unless we want BigTable for scalability purpose.

  2. We use XML for server-client communication. Because it’s debugging friendly.

  3. A set of servers.

    1. Data will be divided up across machines, requiring us to potentially hop from machine to machine.
    2. When possible, we will try to replicate some data across machines to minimize the lookups.
    3. One major design constraint here is to prevent having a single point of failure. For instance, if one machine controlled all the user sign-ins, then we’d cut off millions of users potentially if a single machine lost network connectivity.

Hardest problems

Or the most interesting questions.

Q1: How do we know if someone is online?

While we would like users to tell us when they sign off, we can’t know for sure. A user’s connection might have died, for example. To make sure that we know when a user has signed off, we might try regularly pinging the client to make sure it’s still there.

Q2: How do we deal with conflicting information?

We have some information stored in the computer’s memory and some in the database. What happens if they get out of sync? Which one is “right”?

Q3: How do we make our server scale?

While we designed out chat server without worrying—too much- about scalability, in real life this would be a concern. We’d need to split our data across many servers, which would increase our concern about out-of-sync data.

Q4: How we do prevent denial of service attacks?

Clients can push data to us —- what if they try to DOS (denial of service) us? How do we prevent that?

[CC150v5] 8.7 Design Online Chat Server (1)

Question

Explain how you would design a chat server. In particular, provide details about the various back end components, classes, and methods.

What would be the hardest problems to solve?

Solution

First, decide the objects and methods. Here we’ll focus on the core user management and conversation aspects.

Class

  1. UserMgmt (business logic)
  2. User (includes basic info, UserStatus, a map of conversation, a map of requests)
  3. UserStatus (on/offline, status message)
  4. Conversation Abstract Class (a list of user and a list of messages)
    1. PrivateChat (private conversation)
    2. GroupChat
  5. Message (a string and a date/time)
  6. Request (add request and delete request, involves 2 Users)

Functions

  1. Sign in and log off (update availability)
  2. update status message
  3. add/delete request
  4. send/accept/reject a request
  5. create a conversation (group or private)
  6. add a new message (group or private)

Code

The most important classes is User and UserMgmt. The others are simply data containers.

UserManager.java

public class UserManager {
    private static UserManager instance;
    private HashMap<Integer, User> usersById = new HashMap<Integer, User>();
    private HashMap<String, User> usersByAccountName = new HashMap<String, User>();
    private HashMap<Integer, User> onlineUsers = new HashMap<Integer, User>();

    public static UserManager getInstance() {
        if (instance == null) {
            instance = new UserManager();
        }
        return instance;
    }

    public void addUser(User fromUser, String toAccountName) {
        User toUser = usersByAccountName.get(toAccountName);
        AddRequest req = new AddRequest(fromUser, toUser, new Date());
        toUser.receivedAddRequest(req);
        fromUser.sentAddRequest(req);
    }

    public void approveAddRequest(AddRequest req) {
        req.status = RequestStatus.Accepted;
        User from = req.getFromUser();
        User to = req.getToUser();
        from.addContact(to);
        to.addContact(from);
    }

    public void rejectAddRequest(AddRequest req) {
        req.status = RequestStatus.Rejected;
        User from = req.getFromUser();
        User to = req.getToUser();
        from.removeAddRequest(req);
        to.removeAddRequest(req);       
    }

    public void userSignedOn(String accountName) {
        User user = usersByAccountName.get(accountName);
        if (user != null) {
            user.setStatus(new UserStatus(UserStatusType.Available, ""));           
            onlineUsers.put(user.getId(), user);
        }
    }

    public void userSignedOff(String accountName) {
        User user = usersByAccountName.get(accountName);
        if (user != null) {
            user.setStatus(new UserStatus(UserStatusType.Offline, ""));
            onlineUsers.remove(user.getId());
        }
    }   
}

User.java

Property:

  1. id and name
  2. a map of conversations
  3. a map of sent request
  4. a map of received request
  5. a map of friends list

Methods:

  1. sendMessageToUser(User)
  2. addContact(User)
  3. receivedAddRequest(Request)
  4. sentAddRequest(Request)
  5. removeAddRequest(Request)
  6. addConversation(Conversation)

Note that all user actions are controlled by the UserManager Class. For example, when adding a friend:

  1. User A clicks “add user” on the client.
  2. User A calls requestAddUser (User B).
  3. This method calls UserManager.addUser(User a, userBid).
  4. UserManager calls both User A.sentAddRequest() and User B.receivedAddRequest().

Code:

public class User {
    private int id;
    private UserStatus status = null;
    private HashMap<Integer, PrivateChat> privateChats = new HashMap<Integer, PrivateChat>();
    private ArrayList<GroupChat> groupChats = new ArrayList<GroupChat>();
    private HashMap<Integer, AddRequest> receivedAddRequests = new HashMap<Integer, AddRequest>();
    private HashMap<Integer, AddRequest> sentAddRequests = new HashMap<Integer, AddRequest>();

    private HashMap<Integer, User> contacts = new HashMap<Integer, User>();
    private String accountName;
    private String fullName;

    public User(int id, String accountName, String fullName) {
        this.accountName = accountName;
        this.fullName = fullName;
        this.id = id;
    }

    public boolean sendMessageToUser(User toUser, String content) {
        PrivateChat chat = privateChats.get(toUser.getId());
        if (chat == null) {
            chat = new PrivateChat(this, toUser);
            privateChats.put(toUser.getId(), chat);
        }
        Message message = new Message(content, new Date());
        return chat.addMessage(message);
    }

    public boolean sendMessageToGroupChat(int groupId, String content) {
        GroupChat chat = groupChats.get(groupId);
        if (chat != null) {
            Message message = new Message(content, new Date());
            return chat.addMessage(message);
        }
        return false;
    }

    public void setStatus(UserStatus status) {
        this.status = status;
    }

    public UserStatus getStatus() {
        return status;
    }

    public boolean addContact(User user) {
        if (contacts.containsKey(user.getId())) {
            return false;
        } else {
            contacts.put(user.getId(), user);
            return true;
        }
    }

    public void receivedAddRequest(AddRequest req) {
        int senderId = req.getFromUser().getId();
        if (!receivedAddRequests.containsKey(senderId)) {
            receivedAddRequests.put(senderId, req);
        }       
    }

    public void sentAddRequest(AddRequest req) {
        int receiverId = req.getFromUser().getId();
        if (!sentAddRequests.containsKey(receiverId)) {
            sentAddRequests.put(receiverId, req);
        }       
    }

    public void removeAddRequest(AddRequest req) {
        if (req.getToUser() == this) {
            receivedAddRequests.remove(req);
        } else if (req.getFromUser() == this) {
            sentAddRequests.remove(req);
        }
    }

    public void requestAddUser(String accountName) {
        UserManager.getInstance().addUser(this, accountName);
    }

    public void addConversation(PrivateChat conversation) {
        User otherUser = conversation.getOtherParticipant(this);
        privateChats.put(otherUser.getId(), conversation);
    }

    public void addConversation(GroupChat conversation) {
        groupChats.add(conversation);
    }   

    public int getId() {
        return id;
    }

    public String getAccountName() {
        return accountName;
    }

    public String getFullName() {
        return fullName;
    }
}

[Design] Stack and Heap

Overview

Value types are created on the stack, and reference types are created on the heap.

Both are stored in computer RAM.

Each thread gets a stack, while there’s typically only one heap for the application.

Stack

When a function is called, a block is reserved on the top of the stack for local variables and some bookkeeping data in a LIFO order. Freeing a block from the stack is nothing more than adjusting one pointer.

Heap

Unlike the stack, there’s no enforced pattern to the allocation and deallocation of blocks from the heap; you can allocate a block at any time and free it at any time. This makes it much more complex to keep track of which parts of the heap are allocated or free at any given time; there are many custom heap allocators available to tune heap performance for different usage patterns.

Q & A

What is their scope?

The stack is attached to a thread, so when the thread exits the stack is reclaimed.

The heap is typically allocated at application startup by the runtime, and is reclaimed when the application (technically process) exits.

Thread and process - what’s the difference? Read [Design] Process VS. Thread.

What determines the size of each of them?

The size of the stack is set when a thread is created.

The size of the heap is set on application startup, but can grow as space is needed (the allocator requests more memory from the operating system).

What makes one faster?

The stack is faster because the access pattern makes it trivial to allocate and deallocate memory from it, while the heap has much more complex bookkeeping involved in an allocation or free.

Each byte in the stack tends to be reused very frequently which means it tends to be mapped to the processor’s cache, making it very fast.

Another performance hit for the heap is that the heap, being mostly a global resource, typically has to be multi-threading safe, i.e. each allocation and deallocation needs to be (typically) synchronized.

[Design] Shared Hosting vs. VPS Hosting

Shared hosting

Shared hosting is like living in an apartment where you share a common space with your neighbors. You cannot customize anything but you share maintenance cost and responsibility with your neighbors.

  1. Economical
  2. Technical maintenance of the server is not required
  3. Limited number of resources
  4. Your website performance may be affected by other websites hosted on the shared server
  5. Possible long term problems with scalability and backup
  6. Possible security issues for sharing a common server

Virtual Private Server

Virtual Private Server (VPS) Hosting is like living in a simplex or half-plex where you can customize everything to your own tastes. However, you still need to maintain your own area. Companies that deal with resource-heavy applications and secured data most often use VPS.

  1. Larger space and bandwidth
  2. Can configure anything
  3. Run your own batch files to create multiple services inside the server using shell access
  4. Easy scalability and backup
  5. You need a dedicated system administrator
  6. Costly

[Design] Merits of BST Over HashTables

Question

What are the advantages of binary search trees over hash tables?

Answer

  1. More memory-efficient. (They do not reserve more memory than they need to)

    For instance, if a hash function has a range R(h) = 0…100, then you need to allocate an array of 100 (pointers-to) elements, even if you are just hashing 20 elements.

  2. Inorder traverse.

  3. Collision might hamper HashMap’s performance.

  4. Resizing issue

    When the hash table pressure grows too much, you often tend to enlargen and reallocate the hash table. The BST has simpler behavior here and does not tend to suddenly allocate a lot of data and do a rehashing operation.

  5. Binary search tree do range searches efficiently.

One more thing

Trees tend to be the ultimate average data structure. They can act as lists, can easily be split for parallel operation, have fast removal, insertion and lookup on the order of O(lgn). They do nothing particularly well, but they don’t have any excessively bad behavior either.

[CC150v5] 8.1 Design a Generic Deck of Cards

Question

Design a Generic Deck of Cards

Solution

A simple design:

enum Suit {
    HEART, DIAMOND, SPADES, CLUBS;
}

class Deck {
    List<Card> deck;
}

class Card {
    Suit suit;
    int num;
}

A more complex design:

enum Suit {
    HEART, DIAMOND, SPADES, CLUBS;
}

class Deck<T extends Card> {
    List<Card> deck;

    public void shuffle() {
    };
}

abstract class Card {
    boolean available;
    Suit suit;
    int num;

    public boolean isAvailable() {
        return available;
    };
}

class Hand<T extends Card> {
    List<Card> cards;

    public int score() {
        int score = 0;
        for (Card c : cards) {
            score += c.num;
        }
        return score;
    }

    public void addCard(T card) {
        cards.add(card);
    }
}

// Now use the above generic Data Structure to make a
// Blackjack Game
class Blackjack extends Hand<BlackJackCard> {
}

class BlackJackCard extends Card {
}

[Java OOP] Singleton, 3 Implementations

Implement Singlton

3 ways of writing Singleton.

using Enum

This is only available since Java 6.

public enum Singleton_Enum {
    INSTANCE;
}

using double checked locking

This is lazy loaded thread-safe Singleton, which is popular during Java 5 (with the use of Volatile variable).

public class Singleton_DoubleCheckedLocking implements Cloneable {
    private static volatile Singleton_DoubleCheckedLocking INSTANCE;

    private Singleton_DoubleCheckedLocking() {
    }

    public static Singleton_DoubleCheckedLocking getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton_DoubleCheckedLocking.class) {
                // double checking Singleton instance
                if (INSTANCE == null) {
                    INSTANCE = new Singleton_DoubleCheckedLocking();
                }
            }
        }
        return INSTANCE;
    }
}

using static factory method

Singleton instance is static and final variable it initialized when class is first loaded into memeory so creation of instance is inherently thread-safe.

public class Singleton_StaticFactory {
    // initailzed during class loading
    private static final Singleton_StaticFactory INSTANCE = new Singleton_StaticFactory();

    // to prevent creating another instance of Singleton
    private Singleton_StaticFactory() {
    }

    public static Singleton_StaticFactory getSingleton() {
        return INSTANCE;
    }
}

About thread-safety

Prior to Java 5 double checked locking mechanism is used to create thread-safe singleton in Java, which breaks if one Thread doesn’t see instance created by other thread at same time and eventually you will end up with more than one instance of Singleton class.

From Java 5 onwards volatile variable guarantee can be used to write thread safe singleton by using double checked locking pattern.

I personally don’t prefer that way as there are many other simpler alternatives like:

  1. using static field
  2. using Enum

Q & A

Question: How do you prevent for creating another instance of Singleton using clone() method?

Answer: Preferred way is not to implement Clonnable interface. And if you do, just throw Exception from clone() method as “Can not create clone of Singleton class”.